commit 93d1b7c3d3d9c3c81f7e2bf6b85aaf5ac3410208 Author: huyjaky Date: Wed Apr 22 19:51:20 2026 +0700 first commit diff --git a/.agents/skills/add-admin-api-endpoint/SKILL.md b/.agents/skills/add-admin-api-endpoint/SKILL.md new file mode 100644 index 0000000..5976420 --- /dev/null +++ b/.agents/skills/add-admin-api-endpoint/SKILL.md @@ -0,0 +1,17 @@ +--- +name: Add Admin API Endpoint +description: Add a new endpoint or endpoints to Ghost's Admin API at `ghost/api/admin/**`. +--- + +# Create Admin API Endpoint + +## Instructions + +1. If creating an endpoint for an entirely new resource, create a new endpoint file in `ghost/core/core/server/api/endpoints/`. Otherwise, locate the existing endpoint file in the same directory. +2. The endpoint file should create a controller object using the JSDoc type from (@tryghost/api-framework).Controller, including at minimum a `docName` and a single endpoint definition, i.e. `browse`. +3. Add routes for each endpoint to `ghost/core/core/server/web/api/endpoints/admin/routes.js`. +4. Add basic `e2e-api` tests for the endpoint in `ghost/core/test/e2e-api/admin` to ensure the new endpoints function as expected. +5. Run the tests and iterate until they pass: `cd ghost/core && pnpm test:single test/e2e-api/admin/{test-file-name}`. + +## Reference +For a detailed reference on Ghost's API framework and how to create API controllers, see [reference.md](reference.md). \ No newline at end of file diff --git a/.agents/skills/add-admin-api-endpoint/permissions.md b/.agents/skills/add-admin-api-endpoint/permissions.md new file mode 100644 index 0000000..6966bad --- /dev/null +++ b/.agents/skills/add-admin-api-endpoint/permissions.md @@ -0,0 +1,711 @@ +# API Controller Permissions Guide + +This guide explains how to configure permissions in api-framework controllers, covering all available patterns and best practices. + +## Table of Contents + +- [Overview](#overview) +- [Permission Patterns](#permission-patterns) + - [Boolean `true` - Default Permission Check](#pattern-1-boolean-true---default-permission-check) + - [Boolean `false` - Skip Permissions](#pattern-2-boolean-false---skip-permissions) + - [Function - Custom Permission Logic](#pattern-3-function---custom-permission-logic) + - [Configuration Object - Default with Hooks](#pattern-4-configuration-object---default-with-hooks) +- [The Frame Object](#the-frame-object) +- [Configuration Object Properties](#configuration-object-properties) +- [Complete Examples](#complete-examples) +- [Best Practices](#best-practices) + +--- + +## Overview + +The api-framework uses a **pipeline-based permission system** where permissions are handled as one of five request processing stages: + +1. Input validation +2. Input serialisation +3. **Permissions** ← You are here +4. Query (controller execution) +5. Output serialisation + +**Important**: Every controller method **MUST** explicitly define the `permissions` property. This is a security requirement that prevents accidental security holes and makes permission handling explicit. + +```javascript +// This will throw an IncorrectUsageError +edit: { + query(frame) { + return models.Post.edit(frame.data, frame.options); + } + // Missing permissions property! +} +``` + +--- + +## Permission Patterns + +### Pattern 1: Boolean `true` - Default Permission Check + +The most common pattern that delegates to the default permission handler. + +```javascript +edit: { + headers: { + cacheInvalidate: true + }, + options: ['include'], + validation: { + options: { + include: { + required: true, + values: ['tags'] + } + } + }, + permissions: true, + query(frame) { + return models.Post.edit(frame.data, frame.options); + } +} +``` + +**When to use:** +- Standard CRUD operations +- When the default permission handler meets your needs +- Most common case for authenticated endpoints + +#### How the Default Permission Handler Works + +When you set `permissions: true`, the framework delegates to the default permission handler at `ghost/core/core/server/api/endpoints/utils/permissions.js`. Here's what happens: + +1. **Singular Name Derivation**: The handler converts the `docName` to singular form: + - `posts` → `post` + - `automated_emails` → `automated_email` + - `categories` → `category` (handles `ies` → `y`) + +2. **Permission Check**: It calls the permissions service: + ```javascript + permissions.canThis(frame.options.context)[method][singular](identifier, unsafeAttrs) + ``` + + For example, with `docName: 'posts'` and method `edit`: + ```javascript + permissions.canThis(context).edit.post(postId, unsafeAttrs) + ``` + +3. **Database Lookup**: The permissions service checks the `permissions` and `permissions_roles` tables: + - Looks for a permission with `action_type` matching the method (e.g., `edit`) + - And `object_type` matching the singular docName (e.g., `post`) + - Verifies the user's role has that permission assigned + +#### Required Database Setup + +For the default handler to work, you must have: + +1. **Permission records** in the `permissions` table: + ```sql + INSERT INTO permissions (name, action_type, object_type) VALUES + ('Browse posts', 'browse', 'post'), + ('Read posts', 'read', 'post'), + ('Edit posts', 'edit', 'post'), + ('Add posts', 'add', 'post'), + ('Delete posts', 'destroy', 'post'); + ``` + +2. **Role-permission mappings** in `permissions_roles` linking permissions to roles like Administrator, Editor, etc. + +These are typically added via: +- Initial fixtures in `ghost/core/core/server/data/schema/fixtures/fixtures.json` +- Database migrations using `addPermissionWithRoles()` from `ghost/core/core/server/data/migrations/utils/permissions.js` + +--- + +### Pattern 2: Boolean `false` - Skip Permissions + +Completely bypasses the permissions stage. + +```javascript +browse: { + options: ['page', 'limit'], + permissions: false, + query(frame) { + return models.PublicResource.findAll(frame.options); + } +} +``` + +**When to use:** +- Public endpoints that don't require authentication +- Health check or status endpoints +- Resources that should be accessible to everyone + +**Warning**: Use with caution. Only disable permissions when you're certain the endpoint should be publicly accessible. + +--- + +### Pattern 3: Function - Custom Permission Logic + +Allows complete control over permission validation. + +```javascript +delete: { + options: ['id'], + permissions: async function(frame) { + // Ensure user is authenticated + if (!frame.user || !frame.user.id) { + const UnauthorizedError = require('@tryghost/errors').UnauthorizedError; + return Promise.reject(new UnauthorizedError({ + message: 'You must be logged in to perform this action' + })); + } + + // Only the owner or an admin can delete + const resource = await models.Resource.findOne({id: frame.options.id}); + + if (resource.get('author_id') !== frame.user.id && frame.user.role !== 'admin') { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + return Promise.reject(new NoPermissionError({ + message: 'You do not have permission to delete this resource' + })); + } + + return Promise.resolve(); + }, + query(frame) { + return models.Resource.destroy(frame.options); + } +} +``` + +**When to use:** +- Complex permission logic that varies by resource +- Owner-based permissions +- Role-based access control beyond the default handler +- When you need to query the database for permission decisions + +--- + +### Pattern 4: Configuration Object - Default with Hooks + +Combines default permission handling with configuration options and hooks. + +```javascript +edit: { + options: ['include'], + permissions: { + unsafeAttrs: ['author', 'status'], + before: async function(frame) { + // Load additional user data needed for permission checks + frame.user.permissions = await loadUserPermissions(frame.user.id); + } + }, + query(frame) { + return models.Post.edit(frame.data, frame.options); + } +} +``` + +**When to use:** +- Default permission handler is sufficient but needs configuration +- You have attributes that require special permission handling +- You need to prepare data before permission checks run + +--- + +## The Frame Object + +Permission handlers receive a `frame` object containing complete request context: + +```javascript +Frame { + // Request data + original: {}, // Original untransformed input + options: {}, // Query/URL parameters + data: {}, // Request body + + // User context + user: {}, // Logged-in user object + + // File uploads + file: {}, // Single uploaded file + files: [], // Multiple uploaded files + + // API context + apiType: String, // 'content' or 'admin' + docName: String, // Endpoint name (e.g., 'posts') + method: String, // Method name (e.g., 'browse', 'add', 'edit') + + // HTTP context (added by HTTP wrapper) + context: { + api_key: {}, // API key information + user: userId, // User ID or null + integration: {}, // Integration details + member: {} // Member information or null + } +} +``` + +--- + +## Configuration Object Properties + +When using Pattern 4, these properties are available: + +### `unsafeAttrs` (Array) + +Specifies attributes that require special permission handling. + +```javascript +permissions: { + unsafeAttrs: ['author', 'visibility', 'status'] +} +``` + +These attributes are passed to the permission handler for additional validation. Use this for fields that only certain users should be able to modify (e.g., only admins can change the author of a post). + +### `before` (Function) + +A hook that runs before the default permission handler. + +```javascript +permissions: { + before: async function(frame) { + // Prepare data needed for permission checks + const membership = await loadMembership(frame.user.id); + frame.user.membershipLevel = membership.level; + } +} +``` + +--- + +## Complete Examples + +### Example 1: Public Browse Endpoint + +```javascript +module.exports = { + docName: 'articles', + + browse: { + options: ['page', 'limit', 'filter'], + validation: { + options: { + limit: { + values: [10, 25, 50, 100] + } + } + }, + permissions: false, + query(frame) { + return models.Article.findPage(frame.options); + } + } +}; +``` + +### Example 2: Authenticated CRUD Controller + +```javascript +module.exports = { + docName: 'posts', + + browse: { + options: ['include', 'page', 'limit', 'filter', 'order'], + permissions: true, + query(frame) { + return models.Post.findPage(frame.options); + } + }, + + read: { + options: ['include'], + data: ['id', 'slug'], + permissions: true, + query(frame) { + return models.Post.findOne(frame.data, frame.options); + } + }, + + add: { + headers: { + cacheInvalidate: true + }, + options: ['include'], + permissions: { + unsafeAttrs: ['author_id'] + }, + query(frame) { + return models.Post.add(frame.data.posts[0], frame.options); + } + }, + + edit: { + headers: { + cacheInvalidate: true + }, + options: ['include', 'id'], + permissions: { + unsafeAttrs: ['author_id', 'status'] + }, + query(frame) { + return models.Post.edit(frame.data.posts[0], frame.options); + } + }, + + destroy: { + headers: { + cacheInvalidate: true + }, + options: ['id'], + permissions: true, + statusCode: 204, + query(frame) { + return models.Post.destroy(frame.options); + } + } +}; +``` + +### Example 3: Owner-Based Permissions + +```javascript +module.exports = { + docName: 'user_settings', + + read: { + options: ['user_id'], + permissions: async function(frame) { + // Users can only read their own settings + if (frame.options.user_id !== frame.user.id) { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + return Promise.reject(new NoPermissionError({ + message: 'You can only view your own settings' + })); + } + return Promise.resolve(); + }, + query(frame) { + return models.UserSetting.findOne({user_id: frame.options.user_id}); + } + }, + + edit: { + options: ['user_id'], + permissions: async function(frame) { + // Users can only edit their own settings + if (frame.options.user_id !== frame.user.id) { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + return Promise.reject(new NoPermissionError({ + message: 'You can only edit your own settings' + })); + } + return Promise.resolve(); + }, + query(frame) { + return models.UserSetting.edit(frame.data, frame.options); + } + } +}; +``` + +### Example 4: Role-Based Access Control + +```javascript +module.exports = { + docName: 'admin_settings', + + browse: { + permissions: async function(frame) { + const allowedRoles = ['Owner', 'Administrator']; + + if (!frame.user || !allowedRoles.includes(frame.user.role)) { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + return Promise.reject(new NoPermissionError({ + message: 'Only administrators can access these settings' + })); + } + + return Promise.resolve(); + }, + query(frame) { + return models.AdminSetting.findAll(); + } + }, + + edit: { + permissions: async function(frame) { + // Only the owner can edit admin settings + if (!frame.user || frame.user.role !== 'Owner') { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + return Promise.reject(new NoPermissionError({ + message: 'Only the site owner can modify these settings' + })); + } + + return Promise.resolve(); + }, + query(frame) { + return models.AdminSetting.edit(frame.data, frame.options); + } + } +}; +``` + +### Example 5: Permission with Data Preparation + +```javascript +module.exports = { + docName: 'premium_content', + + read: { + options: ['id'], + permissions: { + before: async function(frame) { + // Load user's subscription status + if (frame.user) { + const subscription = await models.Subscription.findOne({ + user_id: frame.user.id + }); + frame.user.subscription = subscription; + } + } + }, + async query(frame) { + // The query can now use frame.user.subscription + const content = await models.Content.findOne({id: frame.options.id}); + + if (content.get('premium') && !frame.user?.subscription?.active) { + const NoPermissionError = require('@tryghost/errors').NoPermissionError; + throw new NoPermissionError({ + message: 'Premium subscription required' + }); + } + + return content; + } + } +}; +``` + +--- + +## Best Practices + +### 1. Always Define Permissions Explicitly + +```javascript +// Good - explicit about being public +permissions: false + +// Good - explicit about requiring auth +permissions: true + +// Bad - missing permissions (will throw error) +// permissions: undefined +``` + +### 2. Use the Appropriate Pattern + +| Scenario | Pattern | +|----------|---------| +| Public endpoint | `permissions: false` | +| Standard authenticated CRUD | `permissions: true` | +| Need unsafe attrs tracking | `permissions: { unsafeAttrs: [...] }` | +| Complex custom logic | `permissions: async function(frame) {...}` | +| Need pre-processing | `permissions: { before: async function(frame) {...} }` | + +### 3. Keep Permission Logic Focused + +Permission functions should only check permissions, not perform business logic: + +```javascript +// Good - only checks permissions +permissions: async function(frame) { + if (!frame.user || frame.user.role !== 'admin') { + throw new NoPermissionError(); + } +} + +// Bad - mixes permission check with business logic +permissions: async function(frame) { + if (!frame.user) throw new NoPermissionError(); + + // Don't do this in permissions! + frame.data.processed = true; + await sendNotification(frame.user); +} +``` + +### 4. Use Meaningful Error Messages + +```javascript +permissions: async function(frame) { + if (!frame.user) { + throw new UnauthorizedError({ + message: 'Please log in to access this resource' + }); + } + + if (frame.user.role !== 'admin') { + throw new NoPermissionError({ + message: 'Administrator access required for this operation' + }); + } +} +``` + +### 5. Validate Resource Ownership + +When resources belong to specific users, always verify ownership: + +```javascript +permissions: async function(frame) { + const resource = await models.Resource.findOne({id: frame.options.id}); + + if (!resource) { + throw new NotFoundError({message: 'Resource not found'}); + } + + const isOwner = resource.get('user_id') === frame.user.id; + const isAdmin = frame.user.role === 'admin'; + + if (!isOwner && !isAdmin) { + throw new NoPermissionError({ + message: 'You do not have permission to access this resource' + }); + } +} +``` + +### 6. Use `unsafeAttrs` for Sensitive Fields + +Mark fields that require elevated permissions: + +```javascript +permissions: { + unsafeAttrs: [ + 'author_id', // Only admins should change authorship + 'status', // Publishing requires special permission + 'visibility', // Changing visibility is restricted + 'featured' // Only editors can feature content + ] +} +``` + +--- + +## Error Types + +Use appropriate error types from `@tryghost/errors`: + +- **UnauthorizedError** - User is not authenticated +- **NoPermissionError** - User is authenticated but lacks permission +- **NotFoundError** - Resource doesn't exist (use carefully to avoid information leakage) +- **ValidationError** - Input validation failed + +```javascript +const { + UnauthorizedError, + NoPermissionError, + NotFoundError +} = require('@tryghost/errors'); +``` + +--- + +## Adding Permissions via Migrations + +When creating a new API endpoint that uses the default permission handler (`permissions: true`), you need to add permissions to the database. Ghost provides utilities to make this easy. + +### Migration Utilities + +Import the permission utilities from `ghost/core/core/server/data/migrations/utils`: + +```javascript +const {combineTransactionalMigrations, addPermissionWithRoles} = require('../../utils'); +``` + +### Example: Adding CRUD Permissions for a New Resource + +```javascript +// ghost/core/core/server/data/migrations/versions/X.X/YYYY-MM-DD-HH-MM-SS-add-myresource-permissions.js + +const {combineTransactionalMigrations, addPermissionWithRoles} = require('../../utils'); + +module.exports = combineTransactionalMigrations( + addPermissionWithRoles({ + name: 'Browse my resources', + action: 'browse', + object: 'my_resource' // Singular form of docName + }, [ + 'Administrator', + 'Admin Integration' + ]), + addPermissionWithRoles({ + name: 'Read my resources', + action: 'read', + object: 'my_resource' + }, [ + 'Administrator', + 'Admin Integration' + ]), + addPermissionWithRoles({ + name: 'Edit my resources', + action: 'edit', + object: 'my_resource' + }, [ + 'Administrator', + 'Admin Integration' + ]), + addPermissionWithRoles({ + name: 'Add my resources', + action: 'add', + object: 'my_resource' + }, [ + 'Administrator', + 'Admin Integration' + ]), + addPermissionWithRoles({ + name: 'Delete my resources', + action: 'destroy', + object: 'my_resource' + }, [ + 'Administrator', + 'Admin Integration' + ]) +); +``` + +### Available Roles + +Common roles you can assign permissions to: + +- **Administrator** - Full admin access +- **Admin Integration** - API integrations with admin scope +- **Editor** - Can manage all content +- **Author** - Can manage own content +- **Contributor** - Can create drafts only +- **Owner** - Site owner (inherits all Administrator permissions) + +### Permission Naming Conventions + +- **name**: Human-readable, e.g., `'Browse automated emails'` +- **action**: The API method - `browse`, `read`, `edit`, `add`, `destroy` +- **object**: Singular form of `docName` - `automated_email` (not `automated_emails`) + +### Restricting to Administrators Only + +To make an endpoint accessible only to administrators (not editors, authors, etc.), only assign permissions to: +- `Administrator` +- `Admin Integration` + +```javascript +addPermissionWithRoles({ + name: 'Browse sensitive data', + action: 'browse', + object: 'sensitive_data' +}, [ + 'Administrator', + 'Admin Integration' +]) +``` diff --git a/.agents/skills/add-admin-api-endpoint/reference.md b/.agents/skills/add-admin-api-endpoint/reference.md new file mode 100644 index 0000000..d937685 --- /dev/null +++ b/.agents/skills/add-admin-api-endpoint/reference.md @@ -0,0 +1,633 @@ +# Ghost API Framework Reference + +## Overview + +The API framework is a pipeline-based system that processes HTTP requests through a series of stages before executing the controller logic. It provides consistent validation, serialization, and permission handling across all API endpoints. + +## Request Flow + +Each request goes through these stages in order: + +1. **Input Validation** - Validates query params, URL params, and request body +2. **Input Serialization** - Transforms incoming data (e.g., maps `include` to `withRelated`) +3. **Permissions** - Checks if the user/API key has access to the resource +4. **Query** - Executes the actual business logic (your controller code) +5. **Output Serialization** - Formats the response for the client + +## The Frame Object + +The `Frame` class holds all request information and is passed through each stage. Each stage can modify it by reference. + +### Frame Structure + +```javascript +{ + original: Object, // Original input (for debugging) + options: Object, // Query params, URL params, context, custom options + data: Object, // Request body, or query/URL params if configured via `data` + user: Object, // Logged in user object + file: Object, // Single uploaded file + files: Array, // Multiple uploaded files + apiType: String, // 'content' or 'admin' + docName: String, // Endpoint name (e.g., 'posts') + method: String, // Method name (e.g., 'browse', 'read', 'add', 'edit') + response: Object // Set by output serialization +} +``` + +### Frame Example + +```javascript +{ + original: { + include: 'tags,authors' + }, + options: { + withRelated: ['tags', 'authors'], + context: { user: '123' } + }, + data: { + posts: [{ title: 'My Post' }] + } +} +``` + +## API Controller Structure + +Controllers are objects with a `docName` property and method configurations. + +### Basic Structure + +```javascript +module.exports = { + docName: 'posts', // Required: endpoint name + + browse: { + headers: {}, + options: [], + data: [], + validation: {}, + permissions: true, + query(frame) {} + }, + + read: { /* ... */ }, + add: { /* ... */ }, + edit: { /* ... */ }, + destroy: { /* ... */ } +}; +``` + +## Controller Method Properties + +### `headers` (Object) + +Configure HTTP response headers. + +```javascript +headers: { + // Invalidate cache after mutation + cacheInvalidate: true, + // Or with specific path + cacheInvalidate: { value: '/posts/*' }, + + // File disposition for downloads + disposition: { + type: 'csv', // 'csv', 'json', 'yaml', or 'file' + value: 'export.csv' // Can also be a function + }, + + // Location header (auto-generated for 'add' methods) + location: false // Disable auto-generation +} +``` + +### `options` (Array) + +Allowed query/URL parameters that go into `frame.options`. + +```javascript +options: ['include', 'filter', 'page', 'limit', 'order'] +``` + +Can also be a function: +```javascript +options: (frame) => { + return frame.apiType === 'content' + ? ['include'] + : ['include', 'filter']; +} +``` + +### `data` (Array) + +Parameters that go into `frame.data` instead of `frame.options`. Useful for READ requests where the model expects `findOne(data, options)`. + +```javascript +data: ['id', 'slug', 'email'] +``` + +### `validation` (Object | Function) + +Configure input validation. The framework validates against global validators automatically. + +```javascript +validation: { + options: { + include: { + required: true, + values: ['tags', 'authors', 'tiers'] + }, + filter: { + required: false + } + }, + data: { + slug: { + required: true, + values: ['specific-slug'] // Restrict to specific values + } + } +} +``` + +**Global validators** (automatically applied when parameters are present): +- `id` - Must match `/^[a-f\d]{24}$|^1$|me/i` +- `page` - Must be a number +- `limit` - Must be a number or 'all' +- `uuid` - Must be a valid UUID +- `slug` - Must be a valid slug +- `email` - Must be a valid email +- `order` - Must match `/^[a-z0-9_,. ]+$/i` + +For custom validation, use a function: +```javascript +validation(frame) { + if (!frame.data.posts[0].title) { + return Promise.reject(new errors.ValidationError({ + message: 'Title is required' + })); + } +} +``` + +### `permissions` (Boolean | Object | Function) + +**Required field** - you must always specify permissions to avoid security holes. + +```javascript +// Use default permission handling +permissions: true, + +// Skip permission checking (use sparingly!) +permissions: false, + +// With configuration +permissions: { + // Attributes that require elevated permissions + unsafeAttrs: ['status', 'authors'], + + // Run code before permission check + before(frame) { + // Modify frame or do pre-checks + }, + + // Specify which resource type to check against + docName: 'posts', + + // Specify different method for permission check + method: 'browse' +} + +// Custom permission handling +permissions: async function(frame) { + const hasAccess = await checkCustomAccess(frame); + if (!hasAccess) { + return Promise.reject(new errors.NoPermissionError()); + } +} +``` + +### `query` (Function) - Required + +The main business logic. Returns the API response. + +```javascript +query(frame) { + // Access validated options + const { include, filter, page, limit } = frame.options; + + // Access request body + const postData = frame.data.posts[0]; + + // Access context + const userId = frame.options.context.user; + + // Return model response + return models.Post.findPage(frame.options); +} +``` + +### `statusCode` (Number | Function) + +Set the HTTP status code. Defaults to 200. + +```javascript +// Fixed status code +statusCode: 201, + +// Dynamic based on result +statusCode: (result) => { + return result.posts.length ? 200 : 204; +} +``` + +### `response` (Object) + +Configure response format. + +```javascript +response: { + format: 'plain' // Send as plain text instead of JSON +} +``` + +### `cache` (Object) + +Enable endpoint-level caching. + +```javascript +cache: { + async get(cacheKey, fallback) { + const cached = await redis.get(cacheKey); + return cached || await fallback(); + }, + async set(cacheKey, response) { + await redis.set(cacheKey, response, 'EX', 3600); + } +} +``` + +### `generateCacheKeyData` (Function) + +Customize cache key generation. + +```javascript +generateCacheKeyData(frame) { + // Default uses frame.options + return { + ...frame.options, + customKey: 'value' + }; +} +``` + +## Complete Controller Examples + +### Browse Endpoint (List) + +```javascript +browse: { + headers: { + cacheInvalidate: false + }, + options: [ + 'include', + 'filter', + 'fields', + 'formats', + 'page', + 'limit', + 'order' + ], + validation: { + options: { + include: { + values: ['tags', 'authors', 'tiers'] + }, + formats: { + values: ['html', 'plaintext', 'mobiledoc'] + } + } + }, + permissions: true, + query(frame) { + return models.Post.findPage(frame.options); + } +} +``` + +### Read Endpoint (Single) + +```javascript +read: { + headers: { + cacheInvalidate: false + }, + options: ['include', 'fields', 'formats'], + data: ['id', 'slug'], + validation: { + options: { + include: { + values: ['tags', 'authors'] + } + } + }, + permissions: true, + query(frame) { + return models.Post.findOne(frame.data, frame.options); + } +} +``` + +### Add Endpoint (Create) + +```javascript +add: { + headers: { + cacheInvalidate: true + }, + options: ['include'], + validation: { + options: { + include: { + values: ['tags', 'authors'] + } + }, + data: { + title: { required: true } + } + }, + permissions: { + unsafeAttrs: ['status', 'authors'] + }, + statusCode: 201, + query(frame) { + return models.Post.add(frame.data.posts[0], frame.options); + } +} +``` + +### Edit Endpoint (Update) + +```javascript +edit: { + headers: { + cacheInvalidate: true + }, + options: ['include', 'id'], + validation: { + options: { + include: { + values: ['tags', 'authors'] + }, + id: { + required: true + } + } + }, + permissions: { + unsafeAttrs: ['status', 'authors'] + }, + query(frame) { + return models.Post.edit(frame.data.posts[0], frame.options); + } +} +``` + +### Destroy Endpoint (Delete) + +```javascript +destroy: { + headers: { + cacheInvalidate: true + }, + options: ['id'], + validation: { + options: { + id: { + required: true + } + } + }, + permissions: true, + statusCode: 204, + query(frame) { + return models.Post.destroy(frame.options); + } +} +``` + +### File Upload Endpoint + +```javascript +uploadImage: { + headers: { + cacheInvalidate: false + }, + permissions: { + method: 'add' + }, + query(frame) { + // Access uploaded file + const file = frame.file; + + return imageService.upload({ + path: file.path, + name: file.name, + type: file.type + }); + } +} +``` + +### CSV Export Endpoint + +```javascript +exportCSV: { + headers: { + disposition: { + type: 'csv', + value() { + return `members.${new Date().toISOString()}.csv`; + } + } + }, + options: ['filter'], + permissions: true, + response: { + format: 'plain' + }, + query(frame) { + return membersService.export(frame.options); + } +} +``` + +## Using the Framework + +### HTTP Wrapper + +Wrap controllers for Express routes: + +```javascript +const {http} = require('@tryghost/api-framework'); + +// In routes +router.get('/posts', http(api.posts.browse)); +router.get('/posts/:id', http(api.posts.read)); +router.post('/posts', http(api.posts.add)); +router.put('/posts/:id', http(api.posts.edit)); +router.delete('/posts/:id', http(api.posts.destroy)); +``` + +### Internal API Calls + +Call controllers programmatically: + +```javascript +// With data and options +const result = await api.posts.add( + { posts: [{ title: 'New Post' }] }, // data + { context: { user: userId } } // options +); + +// Options only +const posts = await api.posts.browse({ + filter: 'status:published', + include: 'tags', + context: { user: userId } +}); +``` + +### Custom Validators + +Create endpoint-specific validators in the API utils: + +```javascript +// In api/utils/validators/input/posts.js +module.exports = { + add(apiConfig, frame) { + // Custom validation for posts.add + const post = frame.data.posts[0]; + if (post.status === 'published' && !post.title) { + return Promise.reject(new errors.ValidationError({ + message: 'Published posts must have a title' + })); + } + } +}; +``` + +### Custom Serializers + +Create input/output serializers: + +```javascript +// Input serializer +module.exports = { + all(apiConfig, frame) { + // Transform include to withRelated + if (frame.options.include) { + frame.options.withRelated = frame.options.include.split(','); + } + } +}; + +// Output serializer +module.exports = { + posts: { + browse(response, apiConfig, frame) { + // Transform model response to API response + frame.response = { + posts: response.data.map(post => serializePost(post)), + meta: { + pagination: response.meta.pagination + } + }; + } + } +}; +``` + +## Common Patterns + +### Checking User Context + +```javascript +query(frame) { + const isAdmin = frame.options.context.user; + const isIntegration = frame.options.context.integration; + const isMember = frame.options.context.member; + + if (isAdmin) { + return models.Post.findPage(frame.options); + } else { + frame.options.filter = 'status:published'; + return models.Post.findPage(frame.options); + } +} +``` + +### Handling Express Response Directly + +For streaming or special responses: + +```javascript +query(frame) { + // Return a function to handle Express response + return function handler(req, res, next) { + const stream = generateStream(); + stream.pipe(res); + }; +} +``` + +### Setting Custom Headers in Query + +```javascript +query(frame) { + // Set headers from within query + frame.setHeader('X-Custom-Header', 'value'); + + return models.Post.findPage(frame.options); +} +``` + +## Error Handling + +Use `@tryghost/errors` for consistent error responses: + +```javascript +const errors = require('@tryghost/errors'); + +query(frame) { + if (!frame.data.posts[0].title) { + throw new errors.ValidationError({ + message: 'Title is required' + }); + } + + if (notFound) { + throw new errors.NotFoundError({ + message: 'Post not found' + }); + } + + if (noAccess) { + throw new errors.NoPermissionError({ + message: 'You do not have permission to access this resource' + }); + } +} +``` + +## Best Practices + +1. **Always specify `permissions`** - Never omit this field, it's a security requirement +2. **Use `options` to whitelist params** - Only allowed params are passed through +3. **Prefer declarative validation** - Use the validation object over custom functions +4. **Set `cacheInvalidate` appropriately** - True for mutations, false for reads +5. **Use `unsafeAttrs` for sensitive fields** - Requires elevated permissions to modify +6. **Return model responses from `query`** - Let serializers handle transformation +7. **Use `data` for READ endpoints** - When the model expects `findOne(data, options)` diff --git a/.agents/skills/add-admin-api-endpoint/validation.md b/.agents/skills/add-admin-api-endpoint/validation.md new file mode 100644 index 0000000..ce23eb1 --- /dev/null +++ b/.agents/skills/add-admin-api-endpoint/validation.md @@ -0,0 +1,747 @@ +# API Controller Validation Guide + +This guide explains how to configure validations in api-framework controllers, covering all available patterns, built-in validators, and best practices. + +## Table of Contents + +- [Overview](#overview) +- [Validation Patterns](#validation-patterns) + - [Object-Based Validation](#pattern-1-object-based-validation) + - [Function-Based Validation](#pattern-2-function-based-validation) +- [Validating Options (Query Parameters)](#validating-options-query-parameters) +- [Validating Data (Request Body)](#validating-data-request-body) +- [Built-in Global Validators](#built-in-global-validators) +- [Method-Specific Validation Behavior](#method-specific-validation-behavior) +- [Complete Examples](#complete-examples) +- [Error Handling](#error-handling) +- [Best Practices](#best-practices) + +--- + +## Overview + +The api-framework uses a **pipeline-based validation system** where validations run as the first processing stage: + +1. **Validation** ← You are here +2. Input serialisation +3. Permissions +4. Query (controller execution) +5. Output serialisation + +Validation ensures that: +- Required fields are present +- Values are in allowed lists +- Data types are correct (IDs, emails, slugs, etc.) +- Request structure is valid before processing + +--- + +## Validation Patterns + +### Pattern 1: Object-Based Validation + +The most common pattern using configuration objects: + +```javascript +browse: { + options: ['include', 'page', 'limit'], + validation: { + options: { + include: { + values: ['tags', 'authors'], + required: true + }, + page: { + required: false + } + } + }, + permissions: true, + query(frame) { + return models.Post.findPage(frame.options); + } +} +``` + +**When to use:** +- Standard field validation (required, allowed values) +- Most common case for API endpoints + +--- + +### Pattern 2: Function-Based Validation + +Complete control over validation logic: + +```javascript +add: { + validation(frame) { + const {ValidationError} = require('@tryghost/errors'); + + if (!frame.data.posts || !frame.data.posts.length) { + return Promise.reject(new ValidationError({ + message: 'No posts provided' + })); + } + + const post = frame.data.posts[0]; + + if (!post.title || post.title.length < 3) { + return Promise.reject(new ValidationError({ + message: 'Title must be at least 3 characters' + })); + } + + return Promise.resolve(); + }, + permissions: true, + query(frame) { + return models.Post.add(frame.data.posts[0], frame.options); + } +} +``` + +**When to use:** +- Complex validation logic +- Cross-field validation +- Conditional validation rules +- Custom error messages + +--- + +## Validating Options (Query Parameters) + +Options are URL query parameters and route params. Define allowed options in the `options` array and configure validation rules. + +### Required Fields + +```javascript +browse: { + options: ['filter'], + validation: { + options: { + filter: { + required: true + } + } + }, + permissions: true, + query(frame) { + return models.Post.findAll(frame.options); + } +} +``` + +### Allowed Values + +Two equivalent syntaxes: + +**Object notation:** +```javascript +validation: { + options: { + include: { + values: ['tags', 'authors', 'count.posts'] + } + } +} +``` + +**Array shorthand:** +```javascript +validation: { + options: { + include: ['tags', 'authors', 'count.posts'] + } +} +``` + +### Combined Rules + +```javascript +validation: { + options: { + include: { + values: ['tags', 'authors'], + required: true + }, + status: { + values: ['draft', 'published', 'scheduled'], + required: false + } + } +} +``` + +### Special Behavior: Include Parameter + +The `include` parameter has special handling - invalid values are silently filtered instead of causing an error: + +```javascript +// Request: ?include=tags,invalid_field,authors +// Result: frame.options.include = 'tags,authors' +``` + +This allows for graceful degradation when clients request unsupported includes. + +--- + +## Validating Data (Request Body) + +Data validation applies to request body content. The structure differs based on the HTTP method. + +### For READ Operations + +Data comes from query parameters: + +```javascript +read: { + data: ['id', 'slug'], + validation: { + data: { + slug: { + values: ['featured', 'latest'] + } + } + }, + permissions: true, + query(frame) { + return models.Post.findOne(frame.data, frame.options); + } +} +``` + +### For ADD/EDIT Operations + +Data comes from the request body with a root key: + +```javascript +add: { + validation: { + data: { + title: { + required: true + }, + status: { + required: false + } + } + }, + permissions: true, + query(frame) { + return models.Post.add(frame.data.posts[0], frame.options); + } +} +``` + +**Request body structure:** +```json +{ + "posts": [{ + "title": "My Post", + "status": "draft" + }] +} +``` + +### Root Key Validation + +For ADD/EDIT operations, the framework automatically validates: +1. Root key exists (e.g., `posts`, `users`) +2. Root key contains an array with at least one item +3. Required fields exist and are not null + +--- + +## Built-in Global Validators + +The framework automatically validates common field types using the `@tryghost/validator` package: + +| Field Name | Validation Rule | Example Valid Values | +|------------|-----------------|---------------------| +| `id` | MongoDB ObjectId, `1`, or `me` | `507f1f77bcf86cd799439011`, `me` | +| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` | +| `slug` | URL-safe slug | `my-post-title` | +| `email` | Email format | `user@example.com` | +| `page` | Numeric | `1`, `25` | +| `limit` | Numeric or `all` | `10`, `all` | +| `from` | Date format | `2024-01-15` | +| `to` | Date format | `2024-12-31` | +| `order` | Sort format | `created_at desc`, `title asc` | +| `columns` | Column list | `id,title,created_at` | + +### Fields with No Validation + +These fields skip validation by default: +- `filter` +- `context` +- `forUpdate` +- `transacting` +- `include` +- `formats` +- `name` + +--- + +## Method-Specific Validation Behavior + +Different HTTP methods have different validation behaviors: + +### BROWSE / READ + +- Validates `frame.data` against `apiConfig.data` +- Allows empty data +- Uses global validators for field types + +### ADD + +1. Validates root key exists in `frame.data` +2. Checks required fields are present +3. Checks required fields are not null + +**Error examples:** +- `"No root key ('posts') provided."` +- `"Validation (FieldIsRequired) failed for title"` +- `"Validation (FieldIsInvalid) failed for title"` (when null) + +### EDIT + +1. Performs all ADD validations +2. Validates ID consistency between URL and body + +```javascript +// URL: /posts/123 +// Body: { "posts": [{ "id": "456", ... }] } +// Error: "Invalid id provided." +``` + +### Special Methods + +These methods use specific validation behaviors: +- `changePassword()` - Uses ADD rules +- `resetPassword()` - Uses ADD rules +- `setup()` - Uses ADD rules +- `publish()` - Uses BROWSE rules + +--- + +## Complete Examples + +### Example 1: Simple Browse with Options + +```javascript +module.exports = { + docName: 'posts', + + browse: { + options: ['include', 'page', 'limit', 'filter', 'order'], + validation: { + options: { + include: ['tags', 'authors', 'count.posts'], + page: { + required: false + }, + limit: { + required: false + } + } + }, + permissions: true, + query(frame) { + return models.Post.findPage(frame.options); + } + } +}; +``` + +### Example 2: Read with Data Validation + +```javascript +module.exports = { + docName: 'posts', + + read: { + options: ['include'], + data: ['id', 'slug'], + validation: { + options: { + include: ['tags', 'authors'] + }, + data: { + id: { + required: false + }, + slug: { + required: false + } + } + }, + permissions: true, + query(frame) { + return models.Post.findOne(frame.data, frame.options); + } + } +}; +``` + +### Example 3: Add with Required Fields + +```javascript +module.exports = { + docName: 'users', + + add: { + validation: { + data: { + name: { + required: true + }, + email: { + required: true + }, + password: { + required: true + }, + role: { + required: false + } + } + }, + permissions: true, + query(frame) { + return models.User.add(frame.data.users[0], frame.options); + } + } +}; +``` + +### Example 4: Custom Validation Function + +```javascript +module.exports = { + docName: 'subscriptions', + + add: { + validation(frame) { + const {ValidationError} = require('@tryghost/errors'); + const subscription = frame.data.subscriptions?.[0]; + + if (!subscription) { + return Promise.reject(new ValidationError({ + message: 'No subscription data provided' + })); + } + + // Validate email format + if (!subscription.email || !subscription.email.includes('@')) { + return Promise.reject(new ValidationError({ + message: 'Valid email address is required' + })); + } + + // Validate plan + const validPlans = ['free', 'basic', 'premium']; + if (!validPlans.includes(subscription.plan)) { + return Promise.reject(new ValidationError({ + message: `Plan must be one of: ${validPlans.join(', ')}` + })); + } + + // Cross-field validation + if (subscription.plan !== 'free' && !subscription.payment_method) { + return Promise.reject(new ValidationError({ + message: 'Payment method required for paid plans' + })); + } + + return Promise.resolve(); + }, + permissions: true, + query(frame) { + return models.Subscription.add(frame.data.subscriptions[0], frame.options); + } + } +}; +``` + +### Example 5: Edit with ID Consistency + +```javascript +module.exports = { + docName: 'posts', + + edit: { + options: ['id', 'include'], + validation: { + options: { + include: ['tags', 'authors'] + }, + data: { + title: { + required: false + }, + status: { + values: ['draft', 'published', 'scheduled'] + } + } + }, + permissions: { + unsafeAttrs: ['status', 'author_id'] + }, + query(frame) { + return models.Post.edit(frame.data.posts[0], frame.options); + } + } +}; +``` + +### Example 6: Complex Browse with Multiple Validations + +```javascript +module.exports = { + docName: 'analytics', + + browse: { + options: ['from', 'to', 'interval', 'metrics', 'dimensions'], + validation: { + options: { + from: { + required: true + }, + to: { + required: true + }, + interval: { + values: ['hour', 'day', 'week', 'month'], + required: false + }, + metrics: { + values: ['pageviews', 'visitors', 'sessions', 'bounce_rate'], + required: true + }, + dimensions: { + values: ['page', 'source', 'country', 'device'], + required: false + } + } + }, + permissions: true, + query(frame) { + return analytics.query(frame.options); + } + } +}; +``` + +--- + +## Error Handling + +### Error Types + +Validation errors use types from `@tryghost/errors`: +- **ValidationError** - Field validation failed +- **BadRequestError** - Malformed request structure + +### Error Message Format + +```javascript +// Missing required field +"Validation (FieldIsRequired) failed for title" + +// Invalid value +"Validation (AllowedValues) failed for status" + +// Field is null when required +"Validation (FieldIsInvalid) failed for title" + +// Missing root key +"No root key ('posts') provided." + +// ID mismatch +"Invalid id provided." +``` + +### Custom Error Messages + +When using function-based validation: + +```javascript +validation(frame) { + const {ValidationError} = require('@tryghost/errors'); + + if (!frame.data.email) { + return Promise.reject(new ValidationError({ + message: 'Email address is required', + context: 'Please provide a valid email address to continue', + help: 'Check that the email field is included in your request' + })); + } + + return Promise.resolve(); +} +``` + +--- + +## Best Practices + +### 1. Define All Allowed Options + +Always explicitly list allowed options to prevent unexpected parameters: + +```javascript +// Good - explicit allowed options +options: ['include', 'page', 'limit', 'filter'], + +// Bad - no options defined (might allow anything) +// options: undefined +``` + +### 2. Use Built-in Validators + +Let the framework handle common field types: + +```javascript +// Good - framework validates automatically +options: ['id', 'email', 'slug'] + +// Unnecessary - these are validated by default +validation: { + options: { + id: { matches: /^[a-f\d]{24}$/ } // Already built-in + } +} +``` + +### 3. Mark Required Fields Explicitly + +Be explicit about which fields are required: + +```javascript +validation: { + data: { + title: { required: true }, + slug: { required: false }, + status: { required: false } + } +} +``` + +### 4. Use Array Shorthand for Simple Cases + +When only validating allowed values: + +```javascript +// Shorter and cleaner +validation: { + options: { + include: ['tags', 'authors'], + status: ['draft', 'published'] + } +} + +// Equivalent verbose form +validation: { + options: { + include: { values: ['tags', 'authors'] }, + status: { values: ['draft', 'published'] } + } +} +``` + +### 5. Combine with Permissions + +Validation runs before permissions, ensuring data structure is valid: + +```javascript +edit: { + validation: { + data: { + author_id: { required: false } + } + }, + permissions: { + unsafeAttrs: ['author_id'] // Validated first, then permission-checked + }, + query(frame) { + return models.Post.edit(frame.data.posts[0], frame.options); + } +} +``` + +### 6. Use Custom Functions for Complex Logic + +When validation rules depend on multiple fields or external state: + +```javascript +validation(frame) { + // Date range validation + if (frame.options.from && frame.options.to) { + const from = new Date(frame.options.from); + const to = new Date(frame.options.to); + + if (from > to) { + return Promise.reject(new ValidationError({ + message: 'From date must be before to date' + })); + } + + // Max 30 day range + const diffDays = (to - from) / (1000 * 60 * 60 * 24); + if (diffDays > 30) { + return Promise.reject(new ValidationError({ + message: 'Date range cannot exceed 30 days' + })); + } + } + + return Promise.resolve(); +} +``` + +### 7. Provide Helpful Error Messages + +Make errors actionable for API consumers: + +```javascript +// Good - specific and actionable +"Status must be one of: draft, published, scheduled" + +// Bad - vague +"Invalid status" +``` + +--- + +## Validation Flow Diagram + +``` +HTTP Request + ↓ +Frame Creation + ↓ +Frame Configuration (pick options/data) + ↓ +┌─────────────────────────────┐ +│ VALIDATION STAGE │ +├─────────────────────────────┤ +│ Is validation a function? │ +│ ├─ Yes → Run custom logic │ +│ └─ No → Framework validation│ +│ ├─ Global validators │ +│ ├─ Required fields │ +│ ├─ Allowed values │ +│ └─ Method-specific rules│ +└─────────────────────────────┘ + ↓ +Input Serialisation + ↓ +Permissions + ↓ +Query Execution + ↓ +Output Serialisation + ↓ +HTTP Response +``` diff --git a/.agents/skills/add-private-feature-flag/SKILL.md b/.agents/skills/add-private-feature-flag/SKILL.md new file mode 100644 index 0000000..14f6c06 --- /dev/null +++ b/.agents/skills/add-private-feature-flag/SKILL.md @@ -0,0 +1,28 @@ +--- +name: add-private-feature-flag +description: Use when adding a new private (developer experiments) feature flag to Ghost, including the backend registration and settings UI toggle. +--- + +# Add Private Feature Flag + +## Overview +Adds a new private feature flag to Ghost. Private flags appear in Labs settings under the "Private features" tab, visible only when developer experiments are enabled. + +## Steps + +1. **Add the flag to `ghost/core/core/shared/labs.js`** + - Add the flag name (camelCase string) to the `PRIVATE_FEATURES` array. + +2. **Add a UI toggle in `apps/admin-x-settings/src/components/settings/advanced/labs/private-features.tsx`** + - Add a new entry to the `features` array with `title`, `description`, and `flag` (must match the string in `labs.js`). + +3. **Run tests and update the config API snapshot** + - Unit: `cd ghost/core && pnpm test:single test/unit/shared/labs.test.js` + - Update snapshot and run e2e: `cd ghost/core && UPDATE_SNAPSHOTS=1 pnpm test:single test/e2e-api/admin/config.test.js` + - Review the diff of `ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap` to confirm only your new flag was added. + +## Notes +- No database migration is needed. Labs flags are stored in a single JSON `labs` setting. +- The flag name must be identical in `labs.js`, `private-features.tsx`, and the snapshot. +- Flags are camelCase strings (e.g. `welcomeEmailDesignCustomization`). +- For public beta flags (visible to all users), add to `PUBLIC_BETA_FEATURES` in `labs.js` instead and add the toggle to `apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx`. diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md new file mode 100644 index 0000000..23dbc9b --- /dev/null +++ b/.agents/skills/commit/SKILL.md @@ -0,0 +1,61 @@ +--- +name: commit +description: Commit message formatting and guidelines +--- + +# Commit + +Use this skill whenever the user asks you to create a git commit for the current work. + +## Instructions + +1. Review the current git state before committing: + - `git status` + - `git diff` + - `git log -5 --oneline` +2. Only stage files relevant to the requested change. Do not include unrelated untracked files, generated files, or likely-local artifacts. +3. Always follow Ghost's commit conventions (see below) for commit messages +4. Run `git status --short` after committing and confirm the result. + +## Important +- Do not push to remote unless the user explicitly asks +- Keep commits focused and avoid bundling unrelated changes +- If there are no relevant changes, do not create an empty commit +- If hooks fail, fix the issue and create a new commit. Never bypass hooks. + +## Commit message format + +We have a handful of simple standards for commit messages which help us to generate readable changelogs. Please follow this wherever possible and mention the associated issue number. + +- **1st line:** Max 80 character summary + - Written in past tense e.g. “Fixed the thing” not “Fixes the thing” + - Start with one of: Fixed, Changed, Updated, Improved, Added, Removed, Reverted, Moved, Released, Bumped, Cleaned +- **2nd line:** [Always blank] +- **3rd line:** `ref `, `fixes `, `closes ` or blank +- **4th line:** Why this change was made - the code includes the what, the commit message should describe the context of why - why this, why now, why not something else? + +If your change is **user-facing** please prepend the first line of your commit with **an emoji**. + +Because emoji commits are the release notes, it's important that anything that gets an emoji is a user-facing change that's significant and relevant for end-users to see. + +The first line of an emoji commit message should be from the perspective of the user. For example, 🐛 Fixed a race condition in the members service is technical and tells the user nothing, but 🐛 Fixed a bug causing active members to lose access to paid content tells the user reading the release notes “oh yeah, they fixed that bug I kept hitting.” + +### Main emojis we are using: + +- ✨ Feature +- 🎨 Improvement / change +- 🐛 Bug Fix +- 🌐 i18n (translation) submissions +- 💡 Anything else flagged to users or whoever is writing release notes + +### Example + +``` +✨ Added config flag for disabling page analytics + +ref https://linear.app/tryghost/issue/ENG-1234/ + +- analytics are brand new under development, therefore they need to be behind a flag +- not using the developerExperiments flag as that is already in wide use and we aren't ready to deploy this anywhere yet +- using the term `pageAnalytics` as this was discussed as best reflecting what this does +``` diff --git a/.agents/skills/create-database-migration/SKILL.md b/.agents/skills/create-database-migration/SKILL.md new file mode 100644 index 0000000..30aa8c2 --- /dev/null +++ b/.agents/skills/create-database-migration/SKILL.md @@ -0,0 +1,24 @@ +--- +name: Create database migration +description: Create a database migration to add a table, add columns to an existing table, add a setting, or otherwise change the schema of Ghost's MySQL database. Use this skill whenever the task involves modifying Ghost's database schema — including adding, removing, or renaming columns or tables, adding new settings, creating indexes, updating data, or any change that requires a migration file in ghost/core. Also use when the user references schema.js, knex-migrator, the migrations directory, or asks to "add a field" or "add a column" to any Ghost model/table. Even if the user frames it as a feature or Linear issue, if the implementation requires a schema change, this skill applies. +--- + +# Create Database Migration + +## Instructions + +1. Create a new, empty migration file: `cd ghost/core && pnpm migrate:create `. IMPORTANT: do not create the migration file manually; always use this script to create the initial empty migration file. The slug must be kebab-case (e.g. `add-column-to-posts`). +2. The above command will create a new directory in `ghost/core/core/server/data/migrations/versions` if needed, create the empty migration file with the appropriate name, and bump the core and admin package versions to RC if this is the first migration after a release. +3. Update the migration file with the changes you want to make in the database, following the existing patterns in the codebase. Where appropriate, prefer to use the utility functions in `ghost/core/core/server/data/migrations/utils/*`. +4. Update the schema definition file in `ghost/core/core/server/data/schema/schema.js`, and make sure it aligns with the latest changes from the migration. +5. Test the migration manually: `cd ghost/core && pnpm knex-migrator migrate --v {version directory} --force` +6. If adding or dropping a table, update `ghost/core/core/server/data/exporter/table-lists.js` as appropriate. +7. If adding or dropping a table, also add or remove the table name from the expected tables list in `ghost/core/test/integration/exporter/exporter.test.js`. This test has a hardcoded alphabetically-sorted array of all database tables — it runs in CI integration tests (not unit tests) and will fail if the new table is missing. +8. Run the schema integrity test, and update the hash: `cd ghost/core && pnpm test:single test/unit/server/data/schema/integrity.test.js` +9. Run unit tests in Ghost core, and iterate until they pass: `cd ghost/core && pnpm test:unit` + +## Examples +See [examples.md](examples.md) for example migrations. + +## Rules +See [rules.md](rules.md) for rules that should always be followed when creating database migrations. \ No newline at end of file diff --git a/.agents/skills/create-database-migration/examples.md b/.agents/skills/create-database-migration/examples.md new file mode 100644 index 0000000..63eb3d1 --- /dev/null +++ b/.agents/skills/create-database-migration/examples.md @@ -0,0 +1,17 @@ +# Example database migrations + +## Create a table + +See [add mentions table](../../../ghost/core/core/server/data/migrations/versions/5.31/2023-01-19-07-46-add-mentions-table.js). + +## Add column(s) to an existing table + +See [add source columns to emails table](../../../ghost/core/core/server/data/migrations/versions/5.24/2022-11-21-09-32-add-source-columns-to-emails-table.js). + +## Add a setting + +See [add member track source setting](../../../ghost/core/core/server/data/migrations/versions/5.21/2022-10-27-09-50-add-member-track-source-setting.js) + +## Manipulate data + +See [update newsletter subscriptions](../../../ghost/core/core/server/data/migrations/versions/5.31/2022-12-05-09-56-update-newsletter-subscriptions.js). diff --git a/.agents/skills/create-database-migration/rules.md b/.agents/skills/create-database-migration/rules.md new file mode 100644 index 0000000..b41b5f0 --- /dev/null +++ b/.agents/skills/create-database-migration/rules.md @@ -0,0 +1,33 @@ +# Rules for creating database migrations + +## Migrations must be idempotent + +It must be safe to run the migration twice. It's possible for a migration to stop executing due to external factors, so it must be safe to run the migration again successfully. + +## Migrations must NOT use the model layer + +Migrations are written for a specific version, and when they use the model layer, the asusmption is that they are using the models at that version. In reality, the models are of the version which is being migrated to, not from. This means that breaking changes in the models can inadvertently break migrations. + +## Migrations are Immutable + +Once migrations are on the `main` branch, they're final. If you need to make further changes after merging to main, create a new migration instead. + +## Use utility functions + +Wherever possible, use the utility functions in `ghost/core/core/server/data/migrations/utils`, such as `addTable`, `createTransactionalMigration`, and `addSetting`. These util functions have been tested and already include protections for idempotency, as well as log statements where appropriate to make migrations easier to debug. + +## Migration PRs should be as minimal as possible + +Migration PRs should contain the minimal amount of code to create the migration. Usually this means it should only include: +- the new migration file +- updates to the schema.js file +- updated schema integrity hash tests +- updated exporter table lists (when adding or removing tables) + +## Migrations should be defensive + +Protect against missing data. If a migration crashes, Ghost cannot boot. + +## Migrations should log every code path + +If we have to debug a migration, we need to know what it actually did. Without logging, that's impossible, so ensure all code paths and early returns contain logging. Note: when using the utility functions, logging is typically handled in the utility function itself, so no additional logging statements are necessary. \ No newline at end of file diff --git a/.agents/skills/format-number/SKILL.md b/.agents/skills/format-number/SKILL.md new file mode 100644 index 0000000..85694b4 --- /dev/null +++ b/.agents/skills/format-number/SKILL.md @@ -0,0 +1,60 @@ +--- +name: Format numbers +description: Format numbers using the formatNumber function from Shade whenever someone edits a TSX file. +autoTrigger: + - fileEdit: "**/*.tsx" +--- + +# Format Numbers + +When editing `.tsx` files, ensure all user-facing numbers are formatted using the `formatNumber` utility from `@tryghost/shade`. + +## Import + +```typescript +import {formatNumber} from '@tryghost/shade'; +``` + +## When to use formatNumber + +Use `formatNumber()` when rendering any numeric value that is displayed to the user, including: +- Member counts, visitor counts, subscriber counts +- Email engagement metrics (opens, clicks, bounces) +- Revenue amounts (combine with `centsToDollars()` for monetary values) +- Post analytics (views, link clicks) +- Any count or quantity shown in UI + +## Correct usage + +```tsx +{formatNumber(totalMembers)} +{formatNumber(link.count || 0)} +{`${currencySymbol}${formatNumber(centsToDollars(mrr))}`} +{post.members > 0 ? `+${formatNumber(post.members)}` : '0'} +``` + +## Antipatterns to avoid + +Do NOT use any of these patterns for formatting numbers in TSX files: + +```tsx +// BAD: raw .toLocaleString() +{count.toLocaleString()} + +// BAD: manual Intl.NumberFormat +{new Intl.NumberFormat('en-US').format(count)} + +// BAD: raw number without formatting +{memberCount} + +// BAD: manual regex formatting +{count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} +``` + +## Related utilities + +- `formatPercentage()` - for percentages (e.g., open rates, click rates) +- `abbreviateNumber()` - for compact notation (e.g., 1.2M, 50k) +- `centsToDollars()` - convert cents to dollars before passing to `formatNumber` + +All are imported from `@tryghost/shade`. diff --git a/.claude/skills/add-admin-api-endpoint b/.claude/skills/add-admin-api-endpoint new file mode 120000 index 0000000..5f08756 --- /dev/null +++ b/.claude/skills/add-admin-api-endpoint @@ -0,0 +1 @@ +../../.agents/skills/add-admin-api-endpoint \ No newline at end of file diff --git a/.claude/skills/add-private-feature-flag b/.claude/skills/add-private-feature-flag new file mode 120000 index 0000000..d6050b0 --- /dev/null +++ b/.claude/skills/add-private-feature-flag @@ -0,0 +1 @@ +../../.agents/skills/add-private-feature-flag \ No newline at end of file diff --git a/.claude/skills/commit b/.claude/skills/commit new file mode 120000 index 0000000..c237c5b --- /dev/null +++ b/.claude/skills/commit @@ -0,0 +1 @@ +../../.agents/skills/commit \ No newline at end of file diff --git a/.claude/skills/create-database-migration b/.claude/skills/create-database-migration new file mode 120000 index 0000000..64131a8 --- /dev/null +++ b/.claude/skills/create-database-migration @@ -0,0 +1 @@ +../../.agents/skills/create-database-migration \ No newline at end of file diff --git a/.claude/skills/format-number b/.claude/skills/format-number new file mode 120000 index 0000000..c8aa6f2 --- /dev/null +++ b/.claude/skills/format-number @@ -0,0 +1 @@ +../../.agents/skills/format-number \ No newline at end of file diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..d578cda --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +reviews: + high_level_summary: false + collapse_walkthrough: false + changed_files_summary: false + sequence_diagrams: false + estimate_code_review_effort: false + poem: false + auto_review: + base_branches: + - 6.x diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 0000000..4117f9a --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,8 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "Ghost" + +[setup] +script = ''' +pnpm run setup +''' diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 0000000..1ce9a1a --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,6 @@ +{ + "setup-worktree": [ + "git submodule update --init --recursive", + "pnpm" + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fa9c447 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +node_modules + +.nxcache +.nx + +**/*.log + +build +dist + +coverage + +.eslintcache + +test-results + +tsconfig.tsbuildinfo + +Dockerfile +.dockerignore + +.git +.vscode +.editorconfig +compose.yml + +docker +!docker/**/*.entrypoint.sh +!docker/**/*entrypoint.sh + +ghost/core/core/built/admin + +# Ignore local config files (.json and .jsonc) +ghost/core/config.local.json* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..85d8e0a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.hbs] +insert_final_newline = false + +[{package}.json] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..df14092 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Environment variables for Ghost development with docker compose +## Use this file by running `cp .env.example .env` and then editing the values as needed + +# Docker Compose profiles to enable +## Run `docker compose config --profiles` to see all available profiles +## See https://docs.docker.com/compose/how-tos/profiles/ for more information +# COMPOSE_PROFILES=stripe + +# Debug level to pass to Ghost +# DEBUG= + +# Stripe keys - used to forward Stripe webhooks to Ghost +## Stripe Secret Key: sk_test_******* +# STRIPE_SECRET_KEY= +## Stripe Publishable Key: pk_test_******* +#STRIPE_PUBLISHABLE_KEY= +## Stripe Account ID: acct_1******* +#STRIPE_ACCOUNT_ID= + +# Mailgun SMTP credentials - used with `yarn dev:mailgun` +## SMTP username from Mailgun (often starts with `postmaster@`) +# MAILGUN_SMTP_USER= +## SMTP password from Mailgun +# MAILGUN_SMTP_PASS= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6e92bc4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# enforce unix style line endings +*.js text eol=lf +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.hbs text eol=lf + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f81c546 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,18 @@ +# CODEOWNERS for Ghost Repository +# This file defines code ownership for automatic review assignment +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# E2E Test Ownership +# The top-level e2e directory requires review from designated owners +/e2e/ @9larsons + +# Tinybird Analytics +# Tinybird data pipelines and services require review from designated owners +**/tinybird/ @9larsons @cmraible @evanhahn @troyciesco + +# @tryghost/parse-email-address +/ghost/parse-email-address/ @EvanHahn + +# Inbox Links +ghost/core/core/server/lib/get-inbox-links.ts @EvanHahn +ghost/core/test/unit/server/lib/get-inbox-links.test.ts @EvanHahn diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1620aff --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +report@ghost.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..f7efd50 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,84 @@ +# Contributing to Ghost + +For **help**, **support**, **questions** and **ideas** please use **[our forum](https://forum.ghost.org)** 🚑. + +--- + +## Where to Start + +If you're a developer looking to contribute, but you're not sure where to begin: Check out the [good first issue](https://github.com/TryGhost/Ghost/labels/good%20first%20issue) label on Github, which contains small piece of work that have been specifically flagged as being friendly to new contributors. + +After that, if you're looking for something a little more challenging to sink your teeth into, there's a broader [help wanted](https://github.com/TryGhost/Ghost/labels/help%20wanted) label encompassing issues which need some love. + +If you've got an idea for a new feature, please start by suggesting it in the [forum](https://forum.ghost.org), as adding new features to Ghost first requires generating consensus around a design and spec. + + +## Working on Ghost Core + +If you're going to work on Ghost core you'll need to go through a slightly more involved install and setup process than the usual Ghost CLI version. + +First you'll need to fork [Ghost](https://github.com/tryghost/ghost) to your personal Github account, and then follow the detailed [install from source](https://ghost.org/docs/install/source/) setup guide. + + +### Branching Guide + +`main` on the main repository always contains the latest changes. This means that it is WIP for the next minor version and should NOT be considered stable. Stable versions are tagged using [semantic versioning](http://semver.org/). + +On your local repository, you should always work on a branch to make keeping up-to-date and submitting pull requests easier, but in most cases you should submit your pull requests to `main`. Where necessary, for example if multiple people are contributing on a large feature, or if a feature requires a database change, we make use of feature branches. + + +### Commit Messages + +We have a handful of simple standards for commit messages which help us to generate readable changelogs. Please follow this wherever possible and mention the associated issue number. + +- **1st line:** Max 80 character summary + - Written in past tense e.g. “Fixed the thing” not “Fixes the thing” + - Start with one of: Fixed, Changed, Updated, Improved, Added, Removed, Reverted, Moved, Released, Bumped, Cleaned +- **2nd line:** [Always blank] +- **3rd line:** `ref `, `fixes `, `closes ` or blank +- **4th line:** Why this change was made - the code includes the what, the commit message should describe the context of why - why this, why now, why not something else? + +If your change is **user-facing** please prepend the first line of your commit with **an emoji key**. If the commit is for an alpha feature, no emoji is needed. We are following [gitmoji](https://gitmoji.carloscuesta.me/). + +**Main emojis we are using:** + +- ✨ Feature +- 🎨 Improvement / change +- 🐛 Bug Fix +- 🌐 i18n (translation) submissions [[See Translating Ghost docs for more detail](https://www.notion.so/5af2858289b44f9194f73f8a1e17af59?pvs=25#bef8c9988e294a4b9a6dd624136de36f)] +- 💡 Anything else flagged to users or whoever is writing release notes + +Good commit message examples: [new feature](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f), [bug fix](https://github.com/TryGhost/Ghost/commit/6ef835bb5879421ae9133541ebf8c4e560a4a90e) and [translation](https://github.com/TryGhost/Ghost/commit/83904c1611ae7ab3257b3b7d55f03e50cead62d7). + +**Bumping @tryghost dependencies** + +When bumping `@tryghost/*` dependencies, the first line should follow the above format and say what has changed, not say what has been bumped. + +There is no need to include what modules have changed in the commit message, as this is _very_ clear from the contents of the commit. The commit should focus on surfacing the underlying changes from the dependencies - what actually changed as a result of this dependency bump? + +[Good example](https://github.com/TryGhost/Ghost/commit/95751a0e5fb719bb5bca74cb97fb5f29b225094f) + + + +### Submitting Pull Requests + +We aim to merge any straightforward, well-understood bug fixes or improvements immediately, as long as they pass our tests (run `pnpm test` to check locally). We generally don’t merge new features and larger changes without prior discussion with the core product team for tech/design specification. + +Please provide plenty of context and reasoning around your changes, to help us merge quickly. Closing an already open issue is our preferred workflow. If your PR gets out of date, we may ask you to rebase as you are more familiar with your changes than we will be. + +### Sharing feedback on Documentation + +While the Docs are no longer Open Source, we welcome revisions and ideas on the forum! Please create a Post with your questions or suggestions in the [Contributing to Ghost Category](https://forum.ghost.org/c/contributing/27). Thank you for helping us keep the Docs relevant and up-to-date. + +--- + +## Contributor License Agreement + +By contributing your code to Ghost you grant the Ghost Foundation a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: +(a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution. + +You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions. + +You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license. + +The Ghost Foundation acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b5800c9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# You can add one username per supported platform and one custom link +github: tryghost +open_collective: ghost diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..76f353c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,76 @@ +name: 🐛 Bug report +description: Report reproducible software issues so we can improve +body: + - type: markdown + attributes: + value: | + ## Welcome 👋 + Thank you for taking the time to fill out a bug report 🙂 + + We'll respond as quickly as we can. The more information you provide the easier & quicker it is for us to diagnose the problem. + - type: textarea + id: summary + attributes: + label: Issue Summary + description: Explain roughly what's wrong + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Also tell us, what did you expect to happen? + placeholder: | + 1. This is the first step... + 2. This is the second step, etc. + validations: + required: true + - type: input + id: version + attributes: + label: Ghost Version + validations: + required: true + - type: input + id: node + attributes: + label: Node.js Version + validations: + required: true + - type: input + id: install + attributes: + label: How did you install Ghost? + description: Provide details of your host & operating system + validations: + required: true + - type: dropdown + id: database + attributes: + label: Database type + options: + - MySQL 5.7 + - MySQL 8 + - SQLite3 + - Other + validations: + required: true + - type: input + id: browsers + attributes: + label: Browser & OS version + description: Include this for frontend bugs + - type: textarea + id: logs + attributes: + label: Relevant log / error output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://ghost.org/conduct) + options: + - label: I agree to be friendly and polite to people in this repository + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0f0d47d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: 🚑 Help & Support + url: https://forum.ghost.org + about: Please use the community forum for questions + - name: 💡 Features & Ideas + url: https://forum.ghost.org/c/Ideas + about: Please vote for & post new ideas in the the forum + - name: 📖 Documentation + url: https://ghost.org/docs/ + about: Tutorials & reference guides for themes, the API and more diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d7c63ca --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +Got some code for us? Awesome 🎊! + +Please take a minute to explain the change you're making: +- Why are you making it? +- What does it do? +- Why is this something Ghost users or developers need? + +Please check your PR against these items: + +- [ ] I've read and followed the [Contributor Guide](https://github.com/TryGhost/Ghost/blob/main/.github/CONTRIBUTING.md) +- [ ] I've explained my change +- [ ] I've written an automated test to prove my change works + +We appreciate your contribution! 🙏 diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..cdb5cbc --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,28 @@ +# How to get support for Ghost 👨‍👩‍👧‍👦 + +For **help**, **support**, **questions** and **ideas** please use **[our forum](https://forum.ghost.org)** 🚑. + +Please **_do not_** raise an issue on GitHub. + +We have a **help** category in our **[forum](https://forum.ghost.org/)** where you can get quick answers, +help with debugging weird issues, and general help with any aspect of Ghost. There's also an **ideas** category for feature requests. + +Our extensive **documentation** can be found at https://ghost.org/docs/. + +Please go to https://forum.ghost.org and signup to join our community. +You can create a new account, or signup using Google, Twitter or Facebook. + +Issues which are not bug reports will be closed. + +## Using Ghost(Pro)? + +**Ghost(Pro)** users have access to email support via the support at ghost dot org address. + +## Why not GitHub? + +GitHub is our office, it's the place where our development team does its work. We use the issue list +to keep track of bugs and the features that we are working on. We do this openly for transparency. + +With the forum, you can leverage the knowledge of our wider community to get help with any problems you are +having with Ghost. Please keep in mind that Ghost is FLOSS, and free support is provided by the goodwill +of our wonderful community members. diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md new file mode 100644 index 0000000..3d17eec --- /dev/null +++ b/.github/agents/agentic-workflows.agent.md @@ -0,0 +1,155 @@ +--- +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing +disable-model-invocation: true +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt +- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/github-agentic-workflows.md + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/create-agentic-workflow.md + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/update-agentic-workflow.md + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/debug-agentic-workflow.md + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/upgrade-agentic-workflows.md + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/create-shared-agentic-workflow.md + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Fix Dependabot PRs +**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/dependabot.md + +**Use cases**: +- "Fix the open Dependabot PRs for npm dependencies" +- "Bundle and close the Dependabot PRs for workflow dependencies" +- "Update @playwright/test to fix the Dependabot PR" + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the GitHub repository URLs listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Generate the lock file for a workflow +gh aw compile [workflow-name] + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.49.3/.github/aw/github-agentic-workflows.md for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection +- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 0000000..44a2fbc --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,14 @@ +{ + "entries": { + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "github/gh-aw/actions/setup@v0.51.5": { + "repo": "github/gh-aw/actions/setup", + "version": "v0.51.5", + "sha": "88319be75ab1adc60640307a10e5cf04b3deff1e" + } + } +} diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..332b799 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,20 @@ +codecov: + require_ci_to_pass: true +coverage: + status: + patch: false + project: + default: false + admin-tests: + flags: + - admin-tests + threshold: 0.2% + e2e-tests: + flags: + - e2e-tests + threshold: 0.2% +flags: + admin-tests: + carryforward: true + e2e-tests: + carryforward: true diff --git a/.github/hooks/commit-msg b/.github/hooks/commit-msg new file mode 100755 index 0000000..68f275f --- /dev/null +++ b/.github/hooks/commit-msg @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bash "$(dirname "$0")/commit-msg.bash" "$@" diff --git a/.github/hooks/commit-msg.bash b/.github/hooks/commit-msg.bash new file mode 100755 index 0000000..e0ce8cb --- /dev/null +++ b/.github/hooks/commit-msg.bash @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +# Get the commit message file path from the first argument +commit_msg_file="$1" + +# Read the commit message +commit_msg=$(cat "$commit_msg_file") + +# Colors for output +red='\033[0;31m' +yellow='\033[1;33m' +no_color='\033[0m' + +# Get the first line (subject) +subject=$(echo "$commit_msg" | head -n1) + +# Get the second line +second_line=$(echo "$commit_msg" | sed -n '2p') + +# Get the third line +third_line=$(echo "$commit_msg" | sed -n '3p') + +# Get the rest of the message (body) +body=$(echo "$commit_msg" | tail -n +4) + +# Check subject length (max 80 characters) +if [ ${#subject} -gt 80 ]; then + echo -e "${yellow}Warning: Commit message subject is too long (max 80 characters)${no_color}" + echo -e "Current length: ${#subject} characters" +fi + +# Check if second line is blank +if [ ! -z "$second_line" ]; then + echo -e "${yellow}Warning: Second line should be blank${no_color}" +fi + +# Check third line format +if [ ! -z "$third_line" ]; then + if [[ "$third_line" =~ ^(refs|ref:) ]]; then + echo -e "${red}Error: Third line should not start with 'refs' or 'ref:'${no_color}" >&2 + echo -e "Use 'ref ', 'fixes ', or 'closes ' instead" >&2 + echo -e "${yellow}Press Enter to edit the message...${no_color}" >&2 + read < /dev/tty # Wait for Enter key press from the terminal + + # Get the configured Git editor + editor=$(git var GIT_EDITOR) + if [ -z "$editor" ]; then + editor=${VISUAL:-${EDITOR:-vi}} # Fallback logic similar to Git + fi + + # Re-open the editor on the commit message file, connected to the terminal + $editor "$commit_msg_file" < /dev/tty + + # Re-read the potentially modified commit message after editing + commit_msg=$(cat "$commit_msg_file") + # Need to update related variables as well + subject=$(echo "$commit_msg" | head -n1) + second_line=$(echo "$commit_msg" | sed -n '2p') + third_line=$(echo "$commit_msg" | sed -n '3p') + body=$(echo "$commit_msg" | tail -n +4) + + # Re-check the third line *again* after editing + if [[ "$third_line" =~ ^(refs|ref:) ]]; then + echo -e "${red}Error: Third line still starts with 'refs' or 'ref:'. Commit aborted.${no_color}" >&2 + exit 1 # Abort commit if still invalid + fi + # If fixed, the script will continue to the next checks + fi + + if ! [[ "$third_line" =~ ^(ref|fixes|closes)\ .*$ ]]; then + echo -e "${yellow}Warning: Third line should start with 'ref', 'fixes', or 'closes' followed by an issue link${no_color}" >&2 + fi +fi + +# Check for body content (why explanation) +if [ -z "$body" ]; then + echo -e "${yellow}Warning: Missing explanation of why this change was made${no_color}" + echo -e "The body should explain: why this, why now, why not something else?" +fi + +# Check for emoji in user-facing changes +if [[ "$subject" =~ ^[^[:space:]]*[[:space:]] ]]; then + first_word="${subject%% *}" + if [[ ! "$first_word" =~ ^[[:punct:]] ]]; then + echo -e "${yellow}Warning: User-facing changes should start with an emoji${no_color}" + echo -e "Common emojis: ✨ (Feature), 🎨 (Improvement), 🐛 (Bug Fix), 🌐 (i18n), 💡 (User-facing)" + fi +fi + +# Check for past tense verbs in subject +past_tense_words="Fixed|Changed|Updated|Improved|Added|Removed|Reverted|Moved|Released|Bumped|Cleaned" +if ! echo "$subject" | grep -iE "$past_tense_words" > /dev/null; then + echo -e "${yellow}Warning: Subject line should use past tense${no_color}" + echo -e "Use one of: Fixed, Changed, Updated, Improved, Added, Removed, Reverted, Moved, Released, Bumped, Cleaned" +fi + +exit 0 diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit new file mode 100755 index 0000000..80f6bcb --- /dev/null +++ b/.github/hooks/pre-commit @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec bash "$(dirname "$0")/pre-commit.bash" "$@" diff --git a/.github/hooks/pre-commit.bash b/.github/hooks/pre-commit.bash new file mode 100755 index 0000000..48a3089 --- /dev/null +++ b/.github/hooks/pre-commit.bash @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Modified from https://github.com/chaitanyagupta/gitutils + +[ -n "$CI" ] && exit 0 + +pnpm lint-staged --relative +lintStatus=$? + +if [ $lintStatus -ne 0 ]; then + echo "❌ Linting failed" + exit 1 +fi + +green='\033[0;32m' +no_color='\033[0m' +grey='\033[0;90m' +red='\033[0;31m' + +## +## 1) Check and remove submodules before committing +## + +ROOT_DIR=$(git rev-parse --show-cdup) +SUBMODULES=$(grep path ${ROOT_DIR}.gitmodules | sed 's/^.*path = //') +MOD_SUBMODULES=$(git diff --cached --name-only --ignore-submodules=none | grep -F "$SUBMODULES" || true) + +echo -e "Checking submodules ${grey}(pre-commit hook)${no_color} " + +# If no modified submodules, exit with status code 0, else remove them and continue +if [[ -n "$MOD_SUBMODULES" ]]; then + echo -e "${grey}Removing submodules from commit...${no_color}" + for SUB in $MOD_SUBMODULES + do + git reset --quiet HEAD "$SUB" + echo -e "\t${grey}removed:\t$SUB${no_color}" + done + echo + echo -e "${grey}Submodules removed from commit, continuing...${no_color}" + + # If there are no changes to commit after removing submodules, abort to avoid an empty commit + if output=$(git status --porcelain) && [ -z "$output" ]; then + echo -e "nothing to commit, working tree clean" + exit 1 + fi +else + echo "No submodules in commit, continuing..." +fi + +## +## 2) Suggest shipping a new version of @tryghost/activitypub when changes are detected +## The intent is to ship smaller changes more frequently to production +## + +increment_version() { + local package_json_path=$1 + local version_type=$2 + + local current_version + current_version=$(grep '"version":' "$package_json_path" | awk -F '"' '{print $4}') + + IFS='.' read -r major minor patch <<< "$current_version" + + case "$version_type" in + major) ((major++)); minor=0; patch=0 ;; + minor) ((minor++)); patch=0 ;; + patch) ((patch++)) ;; + *) echo "Invalid version type"; exit 1 ;; + esac + + new_version="$major.$minor.$patch" + + # Update package.json with new version + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$new_version\"/" "$package_json_path" + else + # Linux and others + sed -i -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$new_version\"/" "$package_json_path" + fi + + echo "Updated version to $new_version in $package_json_path" +} + +AP_BUMP_NEEDED=false +MODIFIED_FILES=$(git diff --cached --name-only) + +for FILE in $MODIFIED_FILES; do + if [[ "$FILE" == apps/activitypub/* ]]; then + AP_BUMP_NEEDED=true + break + fi +done + +if [[ "$AP_BUMP_NEEDED" == true ]]; then + echo -e "\nYou have made changes to @tryghost/activitypub." + echo -e "Would you like to ship a new version? (yes)" + read -r new_version tryghost/renovate-config" + ], + // Limit concurrent branches to keep Renovate runs within the 30-minute + // Mend timeout and avoid overwhelming CI with dozens of queued jobs. + // The shared preset disables rate limiting, but Ghost's monorepo is + // large enough that unlimited branches cause timeouts during rebasing. + "branchConcurrentLimit": 10, + // Keep manually-closed immortal/grouped PRs closed unless explicitly + // reopened from the Dependency Dashboard. + "recreateWhen": "never", + // pnpm lockfile generation has been hitting Mend's 3GB memory ceiling. + // Renovate maintainers suggested starting with toolSettings.nodeMaxMemory + // set to 1024MB to reduce pnpm's Node heap usage and keep the overall job + // under the hosted runner limit. + "toolSettings": { + "nodeMaxMemory": 1024 + }, + // We have to disable platform based automerge (forcing renovate to do it manually) + // as otherwise renovate wont follow our schedule + "platformAutomerge": false, + "timezone": "Etc/UTC", + // Restrict Renovate runs to the automerge windows so branch updates + // (rebasing, force-pushes) happen around the same times automerge + // can actually complete, not during the working day when CI is busy. + // Each block starts one hour earlier than the matching automerge + // window so Renovate has time to rebase and open/refresh PRs before + // automerge is eligible to run. + "schedule": [ + // Run all weekend + "* * * * 0,6", + // Run on Monday morning (Sun 23:00 is already covered by weekend) + "* 0-12 * * 1", + // Run on weekday evenings, starting 1 hour earlier than automerge + "* 21-23 * * 1-5", + // Run on early weekday mornings (previous day 23:00 is already + // covered by the evening block above) + "* 0-4 * * 2-6" + ], + "automergeSchedule": [ + // Allow automerge all weekend + "* * * * 0,6", + // Allow automerge on Monday morning + "* 0-12 * * 1", + // Allow automerge overnight on weekday evenings (10pm-4:59am UTC) + "* 22-23 * * 1-5", + "* 0-4 * * 2-6" + ], + "ignoreDeps": [ + // https://github.com/TryGhost/Ghost/commit/2b9e494dfcb95c40f596ccf54ec3151c25d53601 + // `got` 10.x has a Node 10 bug that makes it pretty much unusable for now + "got", + // https://github.com/TryGhost/Ghost/commit/2b9e494dfcb95c40f596ccf54ec3151c25d53601 + // `intl-messageformat` 6.0.0 introduced a breaking change in terms of + // escaping that would be pretty difficult to fix for now + "intl-messageformat", + // https://github.com/TryGhost/Ghost/commit/b2fa84c7ff9bf8e21b0791f268f57e92759a87b1 + // no reason given + "moment", + // https://github.com/TryGhost/Ghost/pull/10672 + // https://github.com/TryGhost/Ghost/issues/10870 + "moment-timezone", + // https://github.com/TryGhost/Admin/pull/1111/files + // Ignored because of a mobiledoc-kit issue but that's now in koenig, can probably be cleaned up + "simple-dom", + // https://github.com/TryGhost/Admin/pull/1111/files + // https://github.com/TryGhost/Ghost/pull/10672 + // These have been ignored since forever + "ember-drag-drop", + "normalize.css", + "validator", + + // https://github.com/TryGhost/Ghost/commit/7ebf2891b7470a1c2ffeddefb2fe5e7a57319df3 + // Changed how modules are loaded, caused a weird error during render + "@embroider/macros", + + // https://github.com/TryGhost/Ghost/commit/a10ad3767f60ed2c8e56feb49e7bf83d9618b2ab + // Caused linespacing issues in the editor, but now it's used in different places + // So not sure if it's relevant - soon we will finish switching to react-codemirror + "codemirror", + + // https://github.com/TryGhost/Ghost/commit/3236891b80988924fbbdb625d30cb64a7bf2afd1 + // ember-cli-code-coverage@2.0.0 broke our code coverage + "ember-cli-code-coverage", + // https://github.com/TryGhost/Ghost/commit/1382e34e42a513c201cb957b7f843369a2ce1b63 + // ember-cli-terser@4.0.2 has a regression that breaks our sourcemaps + "ember-cli-terser" + ], + "ignorePaths": [ + "test", + "ghost/admin/lib/koenig-editor/package.json" + ], + "packageRules": [ + // Always require dashboard approval for major updates + // This was largely to avoid the noise of major updates which were ESM only + // The idea was to check and accept major updates if they were NOT ESM + // But this hasn't been workable with our capacity + // Plus, ESM-only is an edge case in the grand scheme of dependencies + { + "description": "Require dashboard approval for major updates", + "matchUpdateTypes": [ + "major" + ], + "dependencyDashboardApproval": true + }, + + // Group NQL packages separately from other TryGhost packages + { + "groupName": "NQL packages", + "matchPackageNames": [ + "@tryghost/nql", + "@tryghost/nql-lang" + ] + }, + + // Split the broad shared TryGhost group into smaller logical lanes so + // failures in one area (e.g. email rendering) don't block unrelated + // internal package updates from merging. + { + "groupName": "TryGhost runtime packages", + "matchPackageNames": [ + "@tryghost/adapter-base-cache", + "@tryghost/admin-api-schema", + "@tryghost/api-framework", + "@tryghost/bookshelf-plugins", + "@tryghost/database-info", + "@tryghost/debug", + "@tryghost/domain-events", + "@tryghost/errors", + "@tryghost/http-cache-utils", + "@tryghost/job-manager", + "@tryghost/logging", + "@tryghost/metrics", + "@tryghost/mw-error-handler", + "@tryghost/mw-vhost", + "@tryghost/pretty-cli", + "@tryghost/prometheus-metrics", + "@tryghost/promise", + "@tryghost/referrer-parser", + "@tryghost/root-utils", + "@tryghost/security", + "@tryghost/social-urls", + "@tryghost/tpl", + "@tryghost/validator", + "@tryghost/version", + "@tryghost/zip" + ] + }, + { + "groupName": "TryGhost admin support packages", + "matchPackageNames": [ + "@tryghost/color-utils", + "@tryghost/custom-fonts", + "@tryghost/limit-service", + "@tryghost/members-csv", + "@tryghost/timezone-data" + ] + }, + { + "groupName": "TryGhost content and email packages", + "matchPackageNames": [ + "@tryghost/config-url-helpers", + "@tryghost/content-api", + "@tryghost/helpers", + "@tryghost/html-to-mobiledoc", + "@tryghost/html-to-plaintext", + "@tryghost/nodemailer", + "@tryghost/parse-email-address", + "@tryghost/request", + "@tryghost/string", + "@tryghost/url-utils" + ] + }, + { + "groupName": "TryGhost test support packages", + "matchPackageNames": [ + "@tryghost/email-mock-receiver", + "@tryghost/express-test", + "@tryghost/webhook-mock-receiver" + ] + }, + + // Always automerge these packages: + { + "matchPackageNames": [ + // This is a pre-1.0.0 package, but it provides icons + // and is very very regularly updated and seems safe to update + "lucide-react" + ], + "automerge": true + }, + + // Allow internal Docker digest pins to automerge once the relevant + // CI checks have gone green. + { + "description": "Automerge internal Docker digest updates after CI passes", + "matchDatasources": [ + "docker" + ], + "matchPackageNames": [ + "ghost/traffic-analytics", + "tinybirdco/tinybird-local" + ], + "matchUpdateTypes": [ + "digest" + ], + "automerge": true, + "automergeType": "pr" + }, + + // Ignore all ember-related packages in admin + // Our ember codebase is being replaced with react and + // Most of the dependencies have breaking changes and it's too hard to update + // Therefore, we'll leave these as-is for now + { + "groupName": "Disable ember updates", + "matchFileNames": [ + "ghost/admin/package.json" + ], + "matchPackageNames": [ + // `ember-foo` style packages + "/^ember(-|$)/", + // scoped `@ember/*` packages + "/^@ember\\//", + // foo/ember-something style packages + "/\\/ember(-|$)/" + ], + "enabled": false + }, + + // Don't allow css preprocessor updates in admin + { + "groupName": "disable css", + "matchFileNames": [ + "ghost/admin/package.json" + ], + "matchPackageNames": [ + "autoprefixer", + "ember-cli-postcss", + "/^postcss/", + "/^css/" + ], + "enabled": false + } + ] +} diff --git a/.github/scripts/bump-version.js b/.github/scripts/bump-version.js new file mode 100644 index 0000000..200f7f2 --- /dev/null +++ b/.github/scripts/bump-version.js @@ -0,0 +1,44 @@ +const fs = require('fs/promises'); +const exec = require('util').promisify(require('child_process').exec); +const path = require('path'); + +const semver = require('semver'); + +(async () => { + const core = await import('@actions/core'); + const corePackageJsonPath = path.join(__dirname, '../../ghost/core/package.json'); + const corePackageJson = require(corePackageJsonPath); + + const current_version = corePackageJson.version; + console.log(`Current version: ${current_version}`); + + const firstArg = process.argv[2]; + console.log('firstArg', firstArg); + + const buildString = await exec('git rev-parse --short HEAD').then(({stdout}) => stdout.trim()); + + let newVersion; + + if (firstArg === 'canary' || firstArg === 'six') { + const bumpedVersion = semver.inc(current_version, 'minor'); + newVersion = `${bumpedVersion}-pre-g${buildString}`; + } else { + newVersion = `${current_version}-0-g${buildString}`; + } + + newVersion += '+moya'; + console.log('newVersion', newVersion); + + corePackageJson.version = newVersion; + await fs.writeFile(corePackageJsonPath, JSON.stringify(corePackageJson, null, 2)); + + const adminPackageJsonPath = path.join(__dirname, '../../ghost/admin/package.json'); + const adminPackageJson = require(adminPackageJsonPath); + adminPackageJson.version = newVersion; + await fs.writeFile(adminPackageJsonPath, JSON.stringify(adminPackageJson, null, 2)); + + console.log('Version bumped to', newVersion); + + core.setOutput('BUILD_VERSION', newVersion); + core.setOutput('GIT_COMMIT_HASH', buildString); +})(); diff --git a/.github/scripts/check-app-version-bump.js b/.github/scripts/check-app-version-bump.js new file mode 100644 index 0000000..8af78d3 --- /dev/null +++ b/.github/scripts/check-app-version-bump.js @@ -0,0 +1,256 @@ +const fs = require('fs'); +const path = require('path'); +const execFileSync = require('child_process').execFileSync; + +const MONITORED_APPS = { + portal: { + packageName: '@tryghost/portal', + path: 'apps/portal' + }, + sodoSearch: { + packageName: '@tryghost/sodo-search', + path: 'apps/sodo-search' + }, + comments: { + packageName: '@tryghost/comments-ui', + path: 'apps/comments-ui' + }, + announcementBar: { + packageName: '@tryghost/announcement-bar', + path: 'apps/announcement-bar' + }, + signupForm: { + packageName: '@tryghost/signup-form', + path: 'apps/signup-form' + } +}; + +const MONITORED_APP_ENTRIES = Object.entries(MONITORED_APPS); +const MONITORED_APP_PATHS = MONITORED_APP_ENTRIES.map(([, app]) => app.path); + +function runGit(args) { + try { + return execFileSync('git', args, {encoding: 'utf8'}).trim(); + } catch (error) { + const stderr = error.stderr ? error.stderr.toString().trim() : ''; + const stdout = error.stdout ? error.stdout.toString().trim() : ''; + const message = stderr || stdout || error.message; + throw new Error(`Failed to run "git ${args.join(' ')}": ${message}`); + } +} + +function readVersionFromPackageJson(packageJsonContent, sourceLabel) { + let parsedPackageJson; + + try { + parsedPackageJson = JSON.parse(packageJsonContent); + } catch (error) { + throw new Error(`Unable to parse ${sourceLabel}: ${error.message}`); + } + + if (!parsedPackageJson.version || typeof parsedPackageJson.version !== 'string') { + throw new Error(`${sourceLabel} does not contain a valid "version" field`); + } + + return parsedPackageJson.version; +} + +function parseSemver(version) { + const match = version.match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/); + + if (!match) { + throw new Error(`Invalid semver version "${version}"`); + } + + const prerelease = match[4] ? match[4].split('.').map((identifier) => { + if (/^\d+$/.test(identifier)) { + return Number(identifier); + } + + return identifier; + }) : []; + + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + prerelease + }; +} + +function comparePrereleaseIdentifier(a, b) { + const isANumber = typeof a === 'number'; + const isBNumber = typeof b === 'number'; + + if (isANumber && isBNumber) { + if (a === b) { + return 0; + } + + return a > b ? 1 : -1; + } + + if (isANumber) { + return -1; + } + + if (isBNumber) { + return 1; + } + + if (a === b) { + return 0; + } + + return a > b ? 1 : -1; +} + +function compareSemver(a, b) { + const aVersion = parseSemver(a); + const bVersion = parseSemver(b); + + if (aVersion.major !== bVersion.major) { + return aVersion.major > bVersion.major ? 1 : -1; + } + + if (aVersion.minor !== bVersion.minor) { + return aVersion.minor > bVersion.minor ? 1 : -1; + } + + if (aVersion.patch !== bVersion.patch) { + return aVersion.patch > bVersion.patch ? 1 : -1; + } + + const aPrerelease = aVersion.prerelease; + const bPrerelease = bVersion.prerelease; + + if (!aPrerelease.length && !bPrerelease.length) { + return 0; + } + + if (!aPrerelease.length) { + return 1; + } + + if (!bPrerelease.length) { + return -1; + } + + const maxLength = Math.max(aPrerelease.length, bPrerelease.length); + for (let i = 0; i < maxLength; i += 1) { + const aIdentifier = aPrerelease[i]; + const bIdentifier = bPrerelease[i]; + + if (aIdentifier === undefined) { + return -1; + } + + if (bIdentifier === undefined) { + return 1; + } + + const identifierComparison = comparePrereleaseIdentifier(aIdentifier, bIdentifier); + if (identifierComparison !== 0) { + return identifierComparison; + } + } + + return 0; +} + +function getChangedFiles(baseSha, compareSha) { + let mergeBaseSha; + + try { + mergeBaseSha = runGit(['merge-base', baseSha, compareSha]); + } catch (error) { + throw new Error(`Unable to determine merge-base for ${baseSha} and ${compareSha}. Ensure the base branch history is available in the checkout.\n${error.message}`); + } + + return runGit(['diff', '--name-only', mergeBaseSha, compareSha, '--', ...MONITORED_APP_PATHS]) + .split('\n') + .map(file => file.trim()) + .filter(Boolean); +} + +function getChangedApps(changedFiles) { + return MONITORED_APP_ENTRIES + .filter(([, app]) => { + return changedFiles.some((file) => { + return file === app.path || file.startsWith(`${app.path}/`); + }); + }) + .map(([key, app]) => ({key, ...app})); +} + +function getPrVersion(app) { + const packageJsonPath = path.resolve(__dirname, `../../${app.path}/package.json`); + + if (!fs.existsSync(packageJsonPath)) { + throw new Error(`${app.path}/package.json does not exist in this PR`); + } + + return readVersionFromPackageJson( + fs.readFileSync(packageJsonPath, 'utf8'), + `${app.path}/package.json from PR` + ); +} + +function getMainVersion(app) { + return readVersionFromPackageJson( + runGit(['show', `origin/main:${app.path}/package.json`]), + `${app.path}/package.json from main` + ); +} + +function main() { + const baseSha = process.env.PR_BASE_SHA; + const compareSha = process.env.PR_COMPARE_SHA || process.env.GITHUB_SHA; + + if (!baseSha) { + throw new Error('Missing PR_BASE_SHA environment variable'); + } + + if (!compareSha) { + throw new Error('Missing PR_COMPARE_SHA/GITHUB_SHA environment variable'); + } + + const changedFiles = getChangedFiles(baseSha, compareSha); + const changedApps = getChangedApps(changedFiles); + + if (changedApps.length === 0) { + console.log(`No app changes detected. Skipping version bump check.`); + return; + } + + console.log(`Checking version bump for apps: ${changedApps.map(app => app.key).join(', ')}`); + + const failedApps = []; + + for (const app of changedApps) { + const prVersion = getPrVersion(app); + const mainVersion = getMainVersion(app); + + if (compareSemver(prVersion, mainVersion) <= 0) { + failedApps.push( + `${app.key} (${app.packageName}) was changed but version was not bumped above main (${prVersion} <= ${mainVersion}). Please run "pnpm ship" in ${app.path} to bump the package version.` + ); + continue; + } + + console.log(`${app.key} version bump check passed (${prVersion} > ${mainVersion})`); + } + + if (failedApps.length) { + throw new Error(`Version bump checks failed:\n- ${failedApps.join('\n- ')}`); + } + + console.log('All monitored app version bump checks passed.'); +} + +try { + main(); +} catch (error) { + console.error(error.message); + process.exit(1); +} diff --git a/.github/scripts/clean.js b/.github/scripts/clean.js new file mode 100644 index 0000000..e8fbddc --- /dev/null +++ b/.github/scripts/clean.js @@ -0,0 +1,44 @@ +// NOTE: this file can't use any NPM dependencies because it needs to run even if dependencies aren't installed yet or are corrupted +const {execSync} = require('child_process'); + +resetNxCache(); +deleteNodeModules(); +deleteBuildArtifacts(); +console.log('Cleanup complete!'); + +function deleteBuildArtifacts() { + console.log('Deleting all build artifacts...'); + try { + execSync('find ./ghost -type d -name "build" -exec rm -rf \'{}\' +', { + stdio: 'inherit' + }); + execSync('find ./ghost -type f -name "tsconfig.tsbuildinfo" -delete', { + stdio: 'inherit' + }); + } catch (error) { + console.error('Failed to delete build artifacts:', error); + process.exit(1); + } +} + +function deleteNodeModules() { + console.log('Deleting all node_modules directories...'); + try { + execSync('find . -name "node_modules" -type d -prune -exec rm -rf \'{}\' +', { + stdio: 'inherit' + }); + } catch (error) { + console.error('Failed to delete node_modules directories:', error); + process.exit(1); + } +} + +function resetNxCache() { + console.log('Resetting NX cache...'); + try { + execSync('rm -rf .nxcache .nx'); + } catch (error) { + console.error('Failed to reset NX cache:', error); + process.exit(1); + } +} diff --git a/.github/scripts/dependency-inspector.js b/.github/scripts/dependency-inspector.js new file mode 100755 index 0000000..2a9f9c8 --- /dev/null +++ b/.github/scripts/dependency-inspector.js @@ -0,0 +1,710 @@ +#!/usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const jsonc = require('jsonc-parser'); +const { execSync } = require('child_process'); + +/** + * Parse pnpm outdated --json output into an array of + * [packageName, current, wanted, latest, dependencyType] tuples. + * + * pnpm's JSON output is an object keyed by package name: + * { "pkg": { "wanted": "1.0.1", "latest": "2.0.0", "dependencyType": "dependencies" } } + * + * pnpm's JSON output does not include a "current" field — "wanted" + * represents the lockfile-resolved version, so we use it as current. + */ +function parsePnpmOutdatedOutput(stdout) { + if (!stdout || !stdout.trim()) { + return []; + } + + const data = JSON.parse(stdout); + return Object.entries(data).map(([name, info]) => [ + name, + info.wanted, + info.wanted, + info.latest, + info.dependencyType + ]); +} + +/** + * Smart lockfile drift detector that focuses on actionable updates + * and avoids API rate limits by using pnpm's built-in commands where possible + */ + +class LockfileDriftDetector { + constructor() { + this.workspaces = []; + this.directDeps = new Map(); + this.outdatedInfo = []; + this.workspaceStats = new Map(); + this.workspaceDepsCount = new Map(); + this.ignoredWorkspaceDeps = new Set(); + this.renovateIgnoredDeps = new Set(); + + // Parse command line arguments + this.args = process.argv.slice(2); + this.filterSeverity = null; + + // Check for severity filters + if (this.args.includes('--patch')) { + this.filterSeverity = 'patch'; + } else if (this.args.includes('--minor')) { + this.filterSeverity = 'minor'; + } else if (this.args.includes('--major')) { + this.filterSeverity = 'major'; + } + + // Check for help flag + if (this.args.includes('--help') || this.args.includes('-h')) { + this.showHelp(); + process.exit(0); + } + } + + /** + * Show help message + */ + showHelp() { + console.log(` +Dependency Inspector - Smart lockfile drift detector + +Usage: dependency-inspector.js [options] + +Options: + --patch Show all packages with patch updates + --minor Show all packages with minor updates + --major Show all packages with major updates + --help, -h Show this help message + +Without flags, shows high-priority updates sorted by impact. +With a severity flag, shows all packages with that update type. +`); + } + + /** + * Load ignored dependencies from renovate configuration + */ + loadRenovateConfig() { + console.log('🔧 Loading renovate configuration...'); + + try { + // Read renovate.json from project root (two levels up from .github/scripts/) + const renovateConfigPath = path.join(__dirname, '../../.github/renovate.json5'); + const renovateConfig = jsonc.parse(fs.readFileSync(renovateConfigPath, 'utf8')); + + if (renovateConfig.ignoreDeps) { + for (const dep of renovateConfig.ignoreDeps) { + this.renovateIgnoredDeps.add(dep); + } + console.log(`📝 Loaded ${renovateConfig.ignoreDeps.length} ignored dependencies from renovate.json`); + console.log(` Ignored: ${Array.from(this.renovateIgnoredDeps).join(', ')}`); + } else { + console.log('📝 No ignoreDeps found in renovate.json'); + } + } catch (error) { + console.warn('⚠️ Could not load renovate.json:', error.message); + } + } + + /** + * Get all workspace package.json files + */ + async findWorkspaces() { + // Read from project root (two levels up from .github/scripts/) + const rootDir = path.join(__dirname, '../..'); + const rootPackage = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')); + + // Read workspace patterns from pnpm-workspace.yaml (primary) or package.json (fallback) + let workspacePatterns = []; + const pnpmWorkspacePath = path.join(rootDir, 'pnpm-workspace.yaml'); + if (fs.existsSync(pnpmWorkspacePath)) { + const content = fs.readFileSync(pnpmWorkspacePath, 'utf8'); + let inPackages = false; + for (const line of content.split('\n')) { + if (/^packages:/.test(line)) { + inPackages = true; + continue; + } + if (inPackages) { + const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/); + if (match) { + workspacePatterns.push(match[1]); + } else if (/^\S/.test(line)) { + break; + } + } + } + } else { + workspacePatterns = rootPackage.workspaces || []; + } + + console.log('📦 Scanning workspaces...'); + + // Add root package + this.workspaces.push({ + name: rootPackage.name || 'root', + path: '.', + packageJson: rootPackage + }); + + // Find workspace packages + for (const pattern of workspacePatterns) { + const globPattern = path.join(rootDir, pattern.replace(/\*$/, '')); + try { + const dirs = fs.readdirSync(globPattern, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => path.join(globPattern, dirent.name)); + + for (const dir of dirs) { + const packageJsonPath = path.join(dir, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Skip ghost/admin directory but track its dependencies for filtering + if (path.basename(dir) === 'admin' && dir.includes('ghost')) { + console.log(`🚫 Ignoring ghost/admin workspace (tracking deps for filtering)`); + const deps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.peerDependencies, + ...packageJson.optionalDependencies + }; + // Add all ghost/admin dependencies to ignore list + for (const depName of Object.keys(deps || {})) { + this.ignoredWorkspaceDeps.add(depName); + } + continue; + } + + this.workspaces.push({ + name: packageJson.name || path.basename(dir), + path: dir, + packageJson + }); + } catch (e) { + console.warn(`⚠️ Skipped ${packageJsonPath}: ${e.message}`); + } + } + } + } catch (e) { + console.warn(`⚠️ Skipped pattern ${pattern}: ${e.message}`); + } + } + + console.log(`Found ${this.workspaces.length} workspaces`); + return this.workspaces; + } + + /** + * Extract all direct dependencies from workspaces + */ + extractDirectDependencies() { + console.log('🔍 Extracting direct dependencies...'); + + for (const workspace of this.workspaces) { + const { packageJson } = workspace; + const deps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.peerDependencies, + ...packageJson.optionalDependencies + }; + + // Count total dependencies for this workspace + const totalDepsForWorkspace = Object.keys(deps || {}).length; + this.workspaceDepsCount.set(workspace.name, totalDepsForWorkspace); + + for (const [name, range] of Object.entries(deps || {})) { + if (!this.directDeps.has(name)) { + this.directDeps.set(name, new Set()); + } + this.directDeps.get(name).add({ + workspace: workspace.name, + range, + path: workspace.path + }); + } + } + + return this.directDeps; + } + + /** + * Use pnpm outdated to get comprehensive outdated info + * This is much faster and more reliable than manual API calls + */ + async getOutdatedPackages() { + console.log('🔄 Running pnpm outdated (this may take a moment)...'); + + let stdout; + try { + stdout = execSync('pnpm outdated --json', { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output + }); + } catch (error) { + // pnpm outdated exits with code 1 when there are outdated packages + if (error.status === 1 && error.stdout) { + stdout = error.stdout; + } else { + console.error('Failed to run pnpm outdated:', error.message); + return []; + } + } + + return parsePnpmOutdatedOutput(stdout); + } + + /** + * Analyze the severity of version differences + */ + analyzeVersionDrift(current, wanted, latest) { + const parseVersion = (v) => { + const match = v.match(/(\d+)\.(\d+)\.(\d+)/); + if (!match) return { major: 0, minor: 0, patch: 0 }; + return { + major: parseInt(match[1]), + minor: parseInt(match[2]), + patch: parseInt(match[3]) + }; + }; + + const currentVer = parseVersion(current); + const latestVer = parseVersion(latest); + + const majorDiff = latestVer.major - currentVer.major; + const minorDiff = latestVer.minor - currentVer.minor; + const patchDiff = latestVer.patch - currentVer.patch; + + let severity = 'patch'; + let score = patchDiff; + + if (majorDiff > 0) { + severity = 'major'; + score = majorDiff * 1000 + minorDiff * 100 + patchDiff; + } else if (minorDiff > 0) { + severity = 'minor'; + score = minorDiff * 100 + patchDiff; + } + + return { severity, score, majorDiff, minorDiff, patchDiff }; + } + + /** + * Process and categorize outdated packages + */ + processOutdatedPackages(outdatedData) { + console.log('📊 Processing outdated package information...'); + + // Initialize workspace stats + for (const workspace of this.workspaces) { + this.workspaceStats.set(workspace.name, { + total: 0, + major: 0, + minor: 0, + patch: 0, + packages: [], + outdatedPackageNames: new Set() // Track unique package names per workspace + }); + } + + const results = { + direct: [], + transitive: [], + stats: { + total: 0, + major: 0, + minor: 0, + patch: 0 + } + }; + + for (const [packageName, current, wanted, latest, packageType] of outdatedData) { + const isDirect = this.directDeps.has(packageName); + + // Skip packages that are only used by ignored workspaces (like ghost/admin) + if (!isDirect && this.ignoredWorkspaceDeps.has(packageName)) { + continue; + } + + // Skip packages that are ignored by renovate configuration + if (this.renovateIgnoredDeps.has(packageName)) { + continue; + } + + const analysis = this.analyzeVersionDrift(current, wanted, latest); + + const packageInfo = { + name: packageName, + current, + wanted, + latest, + type: packageType || 'dependencies', + isDirect, + ...analysis, + workspaces: isDirect ? Array.from(this.directDeps.get(packageName)) : [] + }; + + // Update workspace statistics for direct dependencies + if (isDirect) { + for (const workspaceInfo of packageInfo.workspaces) { + const stats = this.workspaceStats.get(workspaceInfo.workspace); + if (stats && !stats.outdatedPackageNames.has(packageName)) { + // Only count each package once per workspace + stats.outdatedPackageNames.add(packageName); + stats.total++; + stats[analysis.severity]++; + stats.packages.push({ + name: packageName, + current, + latest, + severity: analysis.severity + }); + } + } + results.direct.push(packageInfo); + } else { + results.transitive.push(packageInfo); + } + + results.stats.total++; + results.stats[analysis.severity]++; + } + + // Deduplicate direct dependencies and count workspace impact + const directDepsMap = new Map(); + for (const pkg of results.direct) { + if (!directDepsMap.has(pkg.name)) { + directDepsMap.set(pkg.name, { + ...pkg, + workspaceCount: pkg.workspaces.length, + impact: pkg.workspaces.length // Number of workspaces affected + }); + } + } + + // Sort by impact: workspace count first, then severity, then score + const sortByImpact = (a, b) => { + // First by number of workspaces (more workspaces = higher priority) + if (a.impact !== b.impact) { + return b.impact - a.impact; + } + // Then by severity + if (a.severity !== b.severity) { + const severityOrder = { major: 3, minor: 2, patch: 1 }; + return severityOrder[b.severity] - severityOrder[a.severity]; + } + // Finally by version drift score + return b.score - a.score; + }; + + results.direct = Array.from(directDepsMap.values()).sort(sortByImpact); + results.transitive.sort((a, b) => { + if (a.severity !== b.severity) { + const severityOrder = { major: 3, minor: 2, patch: 1 }; + return severityOrder[b.severity] - severityOrder[a.severity]; + } + return b.score - a.score; + }); + + return results; + } + + /** + * Display filtered results by severity + */ + displayFilteredResults(results) { + const severityEmoji = { + major: '🔴', + minor: '🟡', + patch: '🟢' + }; + + const emoji = severityEmoji[this.filterSeverity]; + const filterTitle = this.filterSeverity.toUpperCase(); + + console.log(`${emoji} ${filterTitle} UPDATES ONLY:\n`); + + // Filter direct dependencies + const filteredDirect = results.direct.filter(pkg => pkg.severity === this.filterSeverity); + const filteredTransitive = results.transitive.filter(pkg => pkg.severity === this.filterSeverity); + + console.log(`Found ${filteredDirect.length} direct and ${filteredTransitive.length} transitive ${this.filterSeverity} updates.\n`); + + if (filteredDirect.length > 0) { + console.log('📦 DIRECT DEPENDENCIES:'); + console.log('─'.repeat(80)); + + // Sort by workspace impact, then by package name + filteredDirect.sort((a, b) => { + if (a.impact !== b.impact) { + return b.impact - a.impact; + } + return a.name.localeCompare(b.name); + }); + + for (const pkg of filteredDirect) { + const workspaceList = pkg.workspaces.map(w => w.workspace).join(', '); + const impactNote = pkg.workspaceCount > 1 ? ` (${pkg.workspaceCount} workspaces)` : ''; + console.log(` ${emoji} ${pkg.name}: ${pkg.current} → ${pkg.latest}${impactNote}`); + console.log(` Workspaces: ${workspaceList}`); + } + + console.log('\n🚀 UPDATE COMMANDS:'); + console.log('─'.repeat(80)); + for (const pkg of filteredDirect) { + console.log(` pnpm update ${pkg.name}@latest`); + } + } + + if (filteredTransitive.length > 0) { + console.log('\n\n🔄 TRANSITIVE DEPENDENCIES:'); + console.log('─'.repeat(80)); + console.log(' These will likely be updated automatically when you update direct deps.\n'); + + // Sort by package name for easier scanning + filteredTransitive.sort((a, b) => a.name.localeCompare(b.name)); + + for (const pkg of filteredTransitive) { + console.log(` ${emoji} ${pkg.name}: ${pkg.current} → ${pkg.latest}`); + } + } + + // Show workspace-specific breakdown + console.log('\n\n🏢 WORKSPACE BREAKDOWN:'); + console.log('─'.repeat(80)); + + for (const [workspaceName, stats] of this.workspaceStats.entries()) { + const severityCount = stats[this.filterSeverity]; + if (severityCount > 0) { + const packages = stats.packages.filter(p => p.severity === this.filterSeverity); + console.log(`\n 📦 ${workspaceName}: ${severityCount} ${this.filterSeverity} update${severityCount !== 1 ? 's' : ''}`); + + // Show all packages for this workspace with the selected severity + for (const pkg of packages) { + console.log(` ${emoji} ${pkg.name}: ${pkg.current} → ${pkg.latest}`); + } + } + } + + console.log(''); + } + + /** + * Display results in a helpful format + */ + displayResults(results) { + console.log('\n🎯 DEPENDENCY ANALYSIS RESULTS\n'); + + // If filtering by severity, show filtered results + if (this.filterSeverity) { + this.displayFilteredResults(results); + return; + } + + // Workspace-specific statistics + console.log('🏢 WORKSPACE BREAKDOWN:'); + console.log(' Outdated packages per workspace:\n'); + + // Sort workspaces by percentage of outdated packages (descending), then by total count + const sortedWorkspaces = Array.from(this.workspaceStats.entries()) + .sort(([nameA, a], [nameB, b]) => { + const totalA = this.workspaceDepsCount.get(nameA) || 0; + const totalB = this.workspaceDepsCount.get(nameB) || 0; + const percentageA = totalA > 0 ? (a.total / totalA) * 100 : 0; + const percentageB = totalB > 0 ? (b.total / totalB) * 100 : 0; + + // Sort by percentage first, then by total count + if (Math.abs(percentageA - percentageB) > 0.1) { + return percentageB - percentageA; + } + return b.total - a.total; + }); + + for (const [workspaceName, stats] of sortedWorkspaces) { + const totalDeps = this.workspaceDepsCount.get(workspaceName) || 0; + const outdatedCount = stats.total; + const percentage = totalDeps > 0 ? ((outdatedCount / totalDeps) * 100).toFixed(1) : '0.0'; + + if (stats.total === 0) { + console.log(` ✅ ${workspaceName}: All ${totalDeps} dependencies up to date! (0% outdated)`); + } else { + console.log(` 📦 ${workspaceName}: ${outdatedCount}/${totalDeps} outdated (${percentage}%)`); + console.log(` 🔴 Major: ${stats.major} | 🟡 Minor: ${stats.minor} | 🟢 Patch: ${stats.patch}`); + + // Show top 3 most outdated packages for this workspace + const topPackages = stats.packages + .sort((a, b) => { + const severityOrder = { major: 3, minor: 2, patch: 1 }; + return severityOrder[b.severity] - severityOrder[a.severity]; + }) + .slice(0, 3); + + if (topPackages.length > 0) { + console.log(` Top issues: ${topPackages.map(p => { + const emoji = p.severity === 'major' ? '🔴' : p.severity === 'minor' ? '🟡' : '🟢'; + return `${emoji} ${p.name} (${p.current}→${p.latest})`; + }).join(', ')}`); + } + console.log(''); + } + } + console.log(''); + + // Direct dependencies (most actionable) + if (results.direct.length > 0) { + console.log('🎯 DIRECT DEPENDENCIES (High Priority):'); + console.log(' Sorted by impact: workspace count → severity → version drift\n'); + + const topDirect = results.direct.slice(0, 15); + for (const pkg of topDirect) { + const emoji = pkg.severity === 'major' ? '🔴' : pkg.severity === 'minor' ? '🟡' : '🟢'; + const impactEmoji = pkg.workspaceCount >= 5 ? '🌟' : pkg.workspaceCount >= 3 ? '⭐' : ''; + console.log(` ${emoji} ${impactEmoji} ${pkg.name}`); + console.log(` ${pkg.current} → ${pkg.latest} (${pkg.severity})`); + console.log(` Used in ${pkg.workspaceCount} workspace${pkg.workspaceCount !== 1 ? 's' : ''}: ${pkg.workspaces.map(w => w.workspace).join(', ')}`); + console.log(''); + } + + if (results.direct.length > 15) { + console.log(` ... and ${results.direct.length - 15} more direct dependencies\n`); + } + } + + // Sample of most outdated transitive dependencies + if (results.transitive.length > 0) { + console.log('🔄 MOST OUTDATED TRANSITIVE DEPENDENCIES (Lower Priority):'); + console.log(' These will likely be updated automatically when you update direct deps.\n'); + + const topTransitive = results.transitive.slice(0, 10); + for (const pkg of topTransitive) { + const emoji = pkg.severity === 'major' ? '🔴' : pkg.severity === 'minor' ? '🟡' : '🟢'; + console.log(` ${emoji} ${pkg.name}: ${pkg.current} → ${pkg.latest} (${pkg.severity})`); + } + + if (results.transitive.length > 10) { + console.log(` ... and ${results.transitive.length - 10} more transitive dependencies\n`); + } + } + + // Generate update commands for highest impact packages + const topUpdates = results.direct.slice(0, 5); + if (topUpdates.length > 0) { + console.log('🚀 SUGGESTED COMMANDS (highest impact first):'); + for (const pkg of topUpdates) { + const impactNote = pkg.workspaceCount > 1 ? ` (affects ${pkg.workspaceCount} workspaces)` : ''; + console.log(` pnpm update ${pkg.name}@latest${impactNote}`); + } + console.log(''); + } + + const generatedAt = new Date().toISOString(); + const latestCommit = this.getLatestCommitRef(); + + // Summary at the end + console.log('📈 SUMMARY:'); + console.log(` Generated at: ${generatedAt}`); + console.log(` Latest commit: ${latestCommit}`); + console.log(` Total dependencies: ${this.directDeps.size}`); + console.log(` Total outdated: ${results.stats.total}`); + console.log(` Major updates: ${results.stats.major}`); + console.log(` Minor updates: ${results.stats.minor}`); + console.log(` Patch updates: ${results.stats.patch}`); + console.log(` Direct deps: ${results.direct.length}`); + console.log(` Transitive deps: ${results.transitive.length}\n`); + } + + /** + * Get the latest commit reference for the current checkout + */ + getLatestCommitRef() { + try { + return execSync("git log -1 --format='%h %ad %s' --date=iso-strict", { + encoding: 'utf8' + }).trim(); + } catch (error) { + return 'Unavailable'; + } + } + + /** + * Run pnpm audit and display a vulnerability summary + */ + displayAuditSummary() { + console.log('🔒 SECURITY AUDIT:\n'); + + try { + let stdout = ''; + try { + stdout = execSync('pnpm audit --json', { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024 + }); + } catch (error) { + // pnpm audit exits with non-zero when vulnerabilities are found + stdout = error.stdout || ''; + } + + if (!stdout || !stdout.trim()) { + console.log(' ⚠️ Could not parse audit summary\n'); + return; + } + + const data = JSON.parse(stdout); + if (data.metadata && data.metadata.vulnerabilities) { + const v = data.metadata.vulnerabilities; + const total = v.info + v.low + v.moderate + v.high + v.critical; + console.log(` Total vulnerabilities: ${total}`); + console.log(` 🔴 Critical: ${v.critical}`); + console.log(` 🟠 High: ${v.high}`); + console.log(` 🟡 Moderate: ${v.moderate}`); + console.log(` 🟢 Low: ${v.low}`); + if (v.info > 0) { + console.log(` ℹ️ Info: ${v.info}`); + } + console.log(` Total dependencies scanned: ${data.metadata.totalDependencies}\n`); + } else { + console.log(' ⚠️ Could not parse audit summary\n'); + } + } catch (error) { + console.log(` ⚠️ Audit failed: ${error.message}\n`); + } + } + + async run() { + try { + // Change to project root directory to run commands correctly + const rootDir = path.join(__dirname, '../..'); + process.chdir(rootDir); + + this.loadRenovateConfig(); + await this.findWorkspaces(); + this.extractDirectDependencies(); + const outdatedData = await this.getOutdatedPackages(); + + if (outdatedData.length === 0) { + console.log('🎉 All packages are up to date!'); + return; + } + + const results = this.processOutdatedPackages(outdatedData); + this.displayResults(results); + this.displayAuditSummary(); + + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } + } +} + +// Run the detector +const detector = new LockfileDriftDetector(); +detector.run(); diff --git a/.github/scripts/enforce-package-manager.js b/.github/scripts/enforce-package-manager.js new file mode 100644 index 0000000..0fa339a --- /dev/null +++ b/.github/scripts/enforce-package-manager.js @@ -0,0 +1,25 @@ +const userAgent = process.env.npm_config_user_agent || ''; + +if (/\bpnpm\//.test(userAgent)) { + process.exit(0); +} + +const detectedPackageManager = userAgent.split(' ')[0] || 'unknown'; + +console.error(` +Ghost now uses pnpm for dependency installation. + +Detected package manager: ${detectedPackageManager} + +Use one of these instead: + corepack enable pnpm + pnpm install + +Common command replacements: + yarn setup -> pnpm run setup + yarn dev -> pnpm dev + yarn test -> pnpm test + yarn lint -> pnpm lint +`); + +process.exit(1); diff --git a/.github/scripts/release-apps.js b/.github/scripts/release-apps.js new file mode 100755 index 0000000..40655c7 --- /dev/null +++ b/.github/scripts/release-apps.js @@ -0,0 +1,215 @@ +const path = require('path'); +const fs = require('fs/promises'); +const exec = require('util').promisify(require('child_process').exec); +const readline = require('readline/promises'); + +const semver = require('semver'); + +// Maps a package name to the config key in defaults.json +const CONFIG_KEYS = { + '@tryghost/portal': 'portal', + '@tryghost/sodo-search': 'sodoSearch', + '@tryghost/comments-ui': 'comments', + '@tryghost/announcement-bar': 'announcementBar', + '@tryghost/signup-form': 'signupForm' +}; + +const CURRENT_DIR = process.cwd(); + +const packageJsonPath = path.join(CURRENT_DIR, 'package.json'); +const packageJson = require(packageJsonPath); + +const APP_NAME = packageJson.name; +const APP_VERSION = packageJson.version; + +async function safeExec(command) { + try { + return await exec(command); + } catch (err) { + return { + stdout: err.stdout, + stderr: err.stderr + }; + } +} + +async function ensureEnabledApp() { + const ENABLED_APPS = Object.keys(CONFIG_KEYS); + if (!ENABLED_APPS.includes(APP_NAME)) { + console.error(`${APP_NAME} is not enabled, please modify ${__filename}`); + process.exit(1); + } +} + +async function ensureNotOnMain() { + const currentGitBranch = await safeExec(`git branch --show-current`); + if (currentGitBranch.stderr) { + console.error(`There was an error checking the current git branch`) + console.error(`${currentGitBranch.stderr}`); + process.exit(1); + } + + if (currentGitBranch.stdout.trim() === 'main') { + console.error(`The release can not be done on the "main" branch`) + process.exit(1); + } +} + +async function ensureCleanGit() { + const localGitChanges = await safeExec(`git status --porcelain`); + if (localGitChanges.stderr) { + console.error(`There was an error checking the local git status`) + console.error(`${localGitChanges.stderr}`); + process.exit(1); + } + + if (localGitChanges.stdout) { + console.error(`You have local git changes - are you sure you're ready to release?`) + console.error(`${localGitChanges.stdout}`); + process.exit(1); + } +} + +async function getNewVersion() { + const rl = readline.createInterface({input: process.stdin, output: process.stdout}); + const bumpTypeInput = await rl.question('Is this a patch, minor or major (patch)? '); + rl.close(); + const bumpType = bumpTypeInput.trim().toLowerCase() || 'patch'; + if (!['patch', 'minor', 'major'].includes(bumpType)) { + console.error(`Unknown bump type ${bumpTypeInput} - expected one of "patch", "minor, "major"`) + process.exit(1); + } + return semver.inc(APP_VERSION, bumpType); +} + +async function updateConfig(newVersion) { + const defaultConfigPath = path.resolve(__dirname, '../../ghost/core/core/shared/config/defaults.json'); + const defaultConfig = require(defaultConfigPath); + + const configKey = CONFIG_KEYS[APP_NAME]; + + defaultConfig[configKey].version = `${semver.major(newVersion)}.${semver.minor(newVersion)}`; + + await fs.writeFile(defaultConfigPath, JSON.stringify(defaultConfig, null, 4) + '\n'); +} + +async function updatePackageJson(newVersion) { + const newPackageJson = Object.assign({}, packageJson, { + version: newVersion + }); + + await fs.writeFile(packageJsonPath, JSON.stringify(newPackageJson, null, 2) + '\n'); +} + +async function getChangelog(newVersion) { + const rl = readline.createInterface({input: process.stdin, output: process.stdout}); + const i18nChangesInput = await rl.question('Does this release contain i18n updates (Y/n)? '); + rl.close(); + + const i18nChanges = i18nChangesInput.trim().toLowerCase() !== 'n'; + + let changelogItems = []; + + if (i18nChanges) { + changelogItems.push('Updated i18n translations'); + } + + // Restrict git log to only the current directory (the specific app) + const lastFiftyCommits = await safeExec(`git log -n 50 --oneline -- .`); + + if (lastFiftyCommits.stderr) { + console.error(`There was an error getting the last 50 commits`); + process.exit(1); + } + + const lastFiftyCommitsList = lastFiftyCommits.stdout.split('\n'); + const releaseRegex = new RegExp(`Released ${APP_NAME} v${APP_VERSION}`); + const indexOfLastRelease = lastFiftyCommitsList.findIndex((commitLine) => { + const commitMessage = commitLine.slice(11); // Take the hash off the front + return releaseRegex.test(commitMessage); + }); + + if (indexOfLastRelease === -1) { + console.warn(`Could not find commit for previous release. Will include recent commits affecting this app.`); + + // Fallback: get recent commits for this app (last 20) + const recentCommits = await safeExec(`git log -n 20 --pretty=format:"%h%n%B__SPLIT__" -- .`); + if (recentCommits.stderr) { + console.error(`There was an error getting recent commits`); + process.exit(1); + } + + const recentCommitsList = recentCommits.stdout.split('__SPLIT__'); + + const recentCommitsWhichMentionLinear = recentCommitsList.filter((commitBlock) => { + return commitBlock.includes('https://linear.app/ghost'); + }); + + const commitChangelogItems = recentCommitsWhichMentionLinear.map((commitBlock) => { + const lines = commitBlock.split('\n'); + if (!lines.length || !lines[0].trim()) { + return null; // Skip entries with no hash + } + const hash = lines[0].trim(); + return `https://github.com/TryGhost/Ghost/commit/${hash}`; + }).filter(Boolean); // Filter out any null entries + + changelogItems.push(...commitChangelogItems); + } else { + const lastReleaseCommit = lastFiftyCommitsList[indexOfLastRelease]; + const lastReleaseCommitHash = lastReleaseCommit.slice(0, 10); + + // Also restrict this git log to only the current directory (the specific app) + const commitsSinceLastRelease = await safeExec(`git log ${lastReleaseCommitHash}..HEAD --pretty=format:"%h%n%B__SPLIT__" -- .`); + if (commitsSinceLastRelease.stderr) { + console.error(`There was an error getting commits since the last release`); + process.exit(1); + } + const commitsSinceLastReleaseList = commitsSinceLastRelease.stdout.split('__SPLIT__'); + + const commitsSinceLastReleaseWhichMentionLinear = commitsSinceLastReleaseList.filter((commitBlock) => { + return commitBlock.includes('https://linear.app/ghost'); + }); + + const commitChangelogItems = commitsSinceLastReleaseWhichMentionLinear.map((commitBlock) => { + const lines = commitBlock.split('\n'); + if (!lines.length || !lines[0].trim()) { + return null; // Skip entries with no hash + } + const hash = lines[0].trim(); + return `https://github.com/TryGhost/Ghost/commit/${hash}`; + }).filter(Boolean); // Filter out any null entries + + changelogItems.push(...commitChangelogItems); + } + + const changelogList = changelogItems.map(item => ` - ${item}`).join('\n'); + return `Changelog for v${APP_VERSION} -> ${newVersion}: \n${changelogList}`; +} + +async function main() { + await ensureEnabledApp(); + await ensureNotOnMain(); + await ensureCleanGit(); + + console.log(`Running release for ${APP_NAME}`); + console.log(`Current version is ${APP_VERSION}`); + + const newVersion = await getNewVersion(); + + console.log(`Bumping to version ${newVersion}`); + + const changelog = await getChangelog(newVersion); + + await updatePackageJson(newVersion); + await exec(`git add package.json`); + + await updateConfig(newVersion); + await exec(`git add ../../ghost/core/core/shared/config/defaults.json`); + + await exec(`git commit -m 'Released ${APP_NAME} v${newVersion}\n\n${changelog}'`); + + console.log(`Release commit created - please double check it and use "git commit --amend" to make any changes before opening a PR to merge into main`) +} + +main(); diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml new file mode 100644 index 0000000..6980f96 --- /dev/null +++ b/.github/workflows/ci-release.yml @@ -0,0 +1,30 @@ +name: CI (Release) +on: + push: + tags: + - 'v[0-9]*' + +# Tags must never be cancelled — each is a public release +concurrency: + group: ci-release-${{ github.ref_name }} + cancel-in-progress: false + +# Workflow-level permissions set the ceiling for the reusable ci.yml. +# id-token is never in the default token, so it must be granted explicitly +# here — otherwise the ci: job's `permissions:` block exceeds the caller +# workflow's permissions and GitHub rejects the run with startup_failure. +permissions: + actions: read + contents: write + packages: write + id-token: write + +jobs: + ci: + uses: ./.github/workflows/ci.yml + secrets: inherit + permissions: + actions: read + contents: write + packages: write + id-token: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9b29d57 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,1838 @@ +name: CI +on: + pull_request: + types: [opened, synchronize, reopened] + push: + # Ref: GHA Filter pattern syntax: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#filter-pattern-cheat-sheet + # Run on pushes to main, release branches, and previous/future major version branches + branches: + - main + - 'v[0-9]+.*' # Matches any release branch, e.g. v6.0.3, v12.1.0 + - '[0-9]+.x' # Matches any major version branch, e.g. 5.x, 23.x + tags-ignore: + - '**' # Tags handled by ci-release.yml + workflow_call: # Called by ci-release.yml for uninterruptible release CI + +env: + FORCE_COLOR: 1 + HEAD_COMMIT: ${{ github.sha }} + NODE_VERSION: 22.18.0 + # Disable v8-compile-cache to prevent intermittent V8 deserializer crashes + # when multiple parallel Nx workers race to read/write shared bytecode cache + # files. The cache lives in /tmp and is discarded after each run anyway, + # so disabling it has no meaningful performance impact in CI. + # See: https://github.com/nodejs/node/issues/51555 + DISABLE_V8_COMPILE_CACHE: 1 + +concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + job_setup: + name: Setup + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + IS_MAIN: ${{ github.ref == 'refs/heads/main' }} + IS_TAG: ${{ startsWith(github.ref, 'refs/tags/v') }} + IS_DEVELOPMENT: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/6.x' }} + IS_SIX: ${{ github.ref == 'refs/heads/6.x' }} + IS_SIX_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref == '6.x' }} + permissions: + actions: read + contents: read + # Required by dorny/paths-filter, which calls pulls.listFiles on + # pull_request events. Private forks of this repo don't grant this + # implicitly when an explicit permissions block is present, so it must + # be listed here for the Setup job to succeed on those forks. + pull-requests: read + steps: + - name: Checkout current commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ env.HEAD_COMMIT }} + fetch-depth: 0 + # fetch a treeless clone to improve checkout speed, the job will fetch contents later if needed + filter: 'tree:0' + + - name: Output GitHub context + if: env.RUNNER_DEBUG == '1' + run: | + echo "GITHUB_EVENT_NAME: ${{ github.event_name }}" + echo "GITHUB_CONTEXT: ${{ toJson(github.event) }}" + + - name: Set SHAs for Nx Commands + if: env.IS_TAG != 'true' + uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5.0.1 + with: + main-branch-name: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }} + error-on-no-successful-workflow: ${{ env.IS_MAIN == 'true' }} + + - name: Check user org membership + id: check_user_org_membership + if: github.event_name == 'pull_request' + run: | + echo "Looking up: ${{ github.triggering_actor }}" + ENCODED_USERNAME=$(printf '%s' '${{ github.triggering_actor }}' | jq -sRr @uri) + + LOOKUP_USER=$(curl --write-out "%{http_code}" --silent --output /dev/null --location "https://api.github.com/orgs/tryghost/members/$ENCODED_USERNAME" --header "Authorization: Bearer ${{ secrets.CANARY_DOCKER_BUILD }}") + + if [ "$LOOKUP_USER" == "204" ]; then + echo "User is in the org" + echo "is_member=true" >> $GITHUB_OUTPUT + else + echo "User is not in the org" + echo "is_member=false" >> $GITHUB_OUTPUT + fi + + - name: Determine added packages + if: env.IS_TAG != 'true' + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: added + with: + base: ${{ env.NX_BASE }} + filters: | + new-package: + - added: 'ghost/**/package.json' + + - name: Determine changed packages + if: env.IS_TAG != 'true' + uses: AurorNZ/paths-filter@c9dd42e99db87803313ff6f4b1150cc9f6c836af # v5.0.0 + id: changed + with: + base: ${{ env.NX_BASE }} + filters: | + shared: &shared + - '.github/**' + - '.npmrc' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + core: + - *shared + - 'ghost/**' + - '!ghost/admin/**' + - '!ghost/core/core/server/data/tinybird/**' + admin: + - *shared + - 'ghost/admin/**' + admin-x-settings: + - *shared + - 'apps/admin-x-settings/**' + - 'apps/admin-x-design-system/**' + - 'apps/admin-x-framework/**' + - 'apps/shade/**' + activitypub: + - *shared + - 'apps/shade/**' + - 'apps/admin-x-framework/**' + - 'apps/activitypub/**' + announcement-bar: + - *shared + - 'apps/announcement-bar/**' + comments-ui: + - *shared + - 'apps/comments-ui/**' + portal: + - *shared + - 'apps/portal/**' + signup-form: + - *shared + - 'apps/signup-form/**' + sodo-search: + - *shared + - 'apps/sodo-search/**' + tinybird: + - '.github/workflows/ci.yml' + - 'compose.dev.analytics.yaml' + - 'ghost/core/core/server/data/tinybird/**' + - '!ghost/core/core/server/data/tinybird/**/*.md' + tinybird-datafiles: + - 'ghost/core/core/server/data/tinybird/**' + - '!ghost/core/core/server/data/tinybird/**/*.md' + any-code: + - '!**/*.md' + + - name: Define Node test matrix + id: node_matrix + run: | + echo 'matrix=["22.18.0"]' >> $GITHUB_OUTPUT + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - name: Set up Node + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Determine Affected Projects + if: env.IS_TAG != 'true' + id: affected + run: | + AFFECTED_PROJECTS=$(pnpm -s nx show projects --affected --json | tr -d '\n') + echo "affected_projects=$AFFECTED_PROJECTS" >> "$GITHUB_OUTPUT" + + outputs: + affected_projects: ${{ steps.affected.outputs.affected_projects }} + changed_admin: ${{ steps.changed.outputs.admin }} + changed_core: ${{ steps.changed.outputs.core }} + changed_admin_x_settings: ${{ steps.changed.outputs.admin-x-settings }} + changed_activitypub: ${{ steps.changed.outputs.activitypub }} + changed_announcement_bar: ${{ steps.changed.outputs.announcement-bar }} + changed_comments_ui: ${{ steps.changed.outputs.comments-ui }} + changed_portal: ${{ steps.changed.outputs.portal }} + changed_signup_form: ${{ steps.changed.outputs.signup-form }} + changed_sodo_search: ${{ steps.changed.outputs.sodo-search }} + changed_tinybird: ${{ steps.changed.outputs.tinybird }} + changed_tinybird_datafiles: ${{ steps.changed.outputs.tinybird-datafiles }} + changed_any_code: ${{ steps.changed.outputs.any-code }} + changed_new_package: ${{ steps.added.outputs.new-package }} + is_main: ${{ env.IS_MAIN }} + is_tag: ${{ env.IS_TAG }} + is_development: ${{ env.IS_DEVELOPMENT }} + is_six: ${{ env.IS_SIX }} + is_six_pr: ${{ env.IS_SIX_PR }} + member_is_in_org: ${{ steps.check_user_org_membership.outputs.is_member }} + node_version: ${{ env.NODE_VERSION }} + node_test_matrix: ${{ steps.node_matrix.outputs.matrix }} + nx_base: ${{ env.NX_BASE }} + + job_app_version_bump_check: + name: Check app version bump + runs-on: ubuntu-latest + needs: [job_setup] + if: github.event_name == 'pull_request' + steps: + - name: Checkout PR head commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Fetch main branch + run: git fetch --no-tags origin main + + - name: Check app version bump + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_COMPARE_SHA: ${{ github.event.pull_request.head.sha }} + run: node .github/scripts/check-app-version-bump.js + + job_lint: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_any_code == 'true' + name: Lint + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1000 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + with: + path: ghost/**/.eslintcache + key: eslint-cache + + - name: Lint all (tags) + if: needs.job_setup.outputs.is_tag == 'true' + run: pnpm nx run-many -t lint + + - name: Lint affected (branches) + if: needs.job_setup.outputs.is_tag != 'true' + run: pnpm nx affected -t lint + env: + NX_BASE: ${{ needs.job_setup.outputs.nx_base }} + NX_HEAD: ${{ env.HEAD_COMMIT }} + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_i18n: + runs-on: ubuntu-latest + needs: [job_setup] + name: i18n + if: | + needs.job_setup.outputs.is_tag == 'true' + || needs.job_setup.outputs.changed_comments_ui == 'true' + || needs.job_setup.outputs.changed_signup_form == 'true' + || needs.job_setup.outputs.changed_sodo_search == 'true' + || needs.job_setup.outputs.changed_portal == 'true' + || needs.job_setup.outputs.changed_core == 'true' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run i18n tests + run: pnpm nx run @tryghost/i18n:test + + job_admin-tests: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_admin == 'true' + name: Admin tests - Chrome + env: + MOZ_HEADLESS: 1 + JOBS: 1 + CI: true + COVERAGE: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - run: pnpm nx run ghost-admin:test + env: + BROWSER: Chrome + + # Merge coverage reports and upload + - name: Merge Admin test coverage + run: pnpm ember coverage-merge + working-directory: ghost/admin + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: admin-coverage + path: ghost/*/coverage/cobertura-coverage.xml + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_unit-tests: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_any_code == 'true' + strategy: + matrix: + node: ${{ fromJSON(needs.job_setup.outputs.node_test_matrix) }} + name: Unit tests (Node ${{ matrix.node }}) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1000 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - name: Install dependencies + # sqlite3 is an optionalDependency. Without --force, pnpm may skip + # installing/linking it when restoring from a cached store. --force + # ensures all optional deps are installed regardless. + # (ghost core's test:unit job requires sqlite3) + run: pnpm install --frozen-lockfile --force + + - name: Set timezone (non-UTC) + uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0 + with: + timezoneLinux: "America/New_York" + + - name: Run unit tests (tags — all) + if: needs.job_setup.outputs.is_tag == 'true' + run: pnpm nx run-many -t test:unit + env: + FORCE_COLOR: 0 + GHOST_UNIT_TEST_VARIANT: ci + NX_SKIP_LOG_GROUPING: true + logging__level: fatal + + - name: Run unit tests (branches — affected) + if: needs.job_setup.outputs.is_tag != 'true' + run: pnpm nx affected -t test:unit + env: + FORCE_COLOR: 0 + GHOST_UNIT_TEST_VARIANT: ci + NX_SKIP_LOG_GROUPING: true + logging__level: fatal + NX_BASE: ${{ needs.job_setup.outputs.nx_base }} + NX_HEAD: ${{ env.HEAD_COMMIT }} + + - name: Check for unexpected file changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Tests generated unexpected file changes. Commit them before merging:" + git status + git diff + exit 1 + fi + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: matrix.node == env.NODE_VERSION + with: + name: unit-coverage + path: ghost/*/coverage/cobertura-coverage.xml + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_acceptance-tests: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true' + services: + mysql: + image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }} + env: + MYSQL_DATABASE: ghost_testing + MYSQL_ROOT_PASSWORD: root + ports: + - 3306 + options: >- + --tmpfs /var/lib/mysql + --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=12 + redis: + image: redis:7.0 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=12 + strategy: + matrix: + node: ${{ fromJSON(needs.job_setup.outputs.node_test_matrix) }} + env: + - DB: mysql8 + NODE_ENV: testing-mysql + include: + - node: ${{ needs.job_setup.outputs.node_version }} + env: + DB: sqlite3 + NODE_ENV: testing + env: + DB: ${{ matrix.env.DB }} + NODE_ENV: ${{ matrix.env.NODE_ENV }} + name: Acceptance tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - name: Install dependencies + # sqlite3 is an optionalDependency. Without --force, pnpm may skip + # installing/linking it when restoring from a cached store. --force + # ensures all optional deps are installed regardless. + run: | + if [ "${{ matrix.env.DB }}" = "sqlite3" ]; then + pnpm install --frozen-lockfile --force + else + pnpm install --frozen-lockfile + fi + + - name: Set timezone (non-UTC) + uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0 + with: + timezoneLinux: "America/New_York" + + - name: Set env vars (SQLite) + if: contains(matrix.env.DB, 'sqlite') + run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> $GITHUB_ENV + + - name: Set env vars (MySQL) + if: contains(matrix.env.DB, 'mysql') + run: | + echo "database__connection__host=127.0.0.1" >> $GITHUB_ENV + echo "database__connection__port=${{ job.services.mysql.ports['3306'] }}" >> $GITHUB_ENV + echo "database__connection__password=root" >> $GITHUB_ENV + + - name: E2E tests + run: pnpm nx run ghost:test:ci:e2e + + - name: Integration tests + run: pnpm nx run ghost:test:ci:integration + + - name: Check for unexpected file changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Tests generated unexpected file changes. Commit them before merging:" + git status + git diff + exit 1 + fi + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: matrix.node == env.NODE_VERSION && contains(matrix.env.DB, 'mysql') + with: + name: e2e-coverage + path: | + ghost/*/coverage-e2e/cobertura-coverage.xml + ghost/*/coverage-integration/cobertura-coverage.xml + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_legacy-tests: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true' + services: + mysql: + image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }} + env: + MYSQL_DATABASE: ghost_testing + MYSQL_ROOT_PASSWORD: root + ports: + - 3306 + options: >- + --tmpfs /var/lib/mysql + --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=12 + strategy: + matrix: + include: + - node: ${{ needs.job_setup.outputs.node_version }} + env: + DB: mysql8 + NODE_ENV: testing-mysql + - node: ${{ needs.job_setup.outputs.node_version }} + env: + DB: sqlite3 + NODE_ENV: testing + env: + DB: ${{ matrix.env.DB }} + NODE_ENV: ${{ matrix.env.NODE_ENV }} + name: Legacy tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - name: Install dependencies + # sqlite3 is an optionalDependency. Without --force, pnpm may skip + # installing/linking it when restoring from a cached store. --force + # ensures all optional deps are installed regardless. + run: | + if [ "${{ matrix.env.DB }}" = "sqlite3" ]; then + pnpm install --frozen-lockfile --force + else + pnpm install --frozen-lockfile + fi + + - name: Set env vars (SQLite) + if: contains(matrix.env.DB, 'sqlite') + run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> $GITHUB_ENV + + - name: Set env vars (MySQL) + if: contains(matrix.env.DB, 'mysql') + run: | + echo "database__connection__host=127.0.0.1" >> $GITHUB_ENV + echo "database__connection__port=${{ job.services.mysql.ports['3306'] }}" >> $GITHUB_ENV + echo "database__connection__password=root" >> $GITHUB_ENV + + - name: Legacy tests + run: pnpm nx run ghost:test:ci:legacy + + - name: Check for unexpected file changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Tests generated unexpected file changes. Commit them before merging:" + git status + git diff + exit 1 + fi + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_admin_x_settings: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_admin_x_settings == 'true' + name: Admin-X Settings tests + env: + CI: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + + - run: pnpm nx run @tryghost/admin-x-settings:test:acceptance + + - name: Upload test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: admin-x-settings-playwright-report + path: apps/admin-x-settings/playwright-report + retention-days: 30 + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_activitypub: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_activitypub == 'true' + name: ActivityPub tests + env: + CI: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + + - run: pnpm nx run @tryghost/activitypub:test:acceptance + + - name: Upload test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: activitypub-playwright-report + path: apps/activitypub/playwright-report + retention-days: 30 + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_comments_ui: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_comments_ui == 'true' + name: Comments-UI tests + env: + CI: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + + - run: pnpm nx run @tryghost/comments-ui:test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: comments-ui-playwright-report + path: apps/comments-ui/playwright-report + retention-days: 30 + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_signup_form: + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_signup_form == 'true' + name: Signup-form tests + env: + CI: true + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + + - run: pnpm nx run @tryghost/signup-form:test:e2e + + - name: Upload test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: signup-form-playwright-report + path: apps/signup-form/playwright-report + retention-days: 30 + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_tinybird-tests: + name: Tinybird Tests + runs-on: ubuntu-latest + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_tinybird == 'true' + defaults: + run: + working-directory: ghost/core/core/server/data/tinybird + services: + tinybird: + image: tinybirdco/tinybird-local:latest@sha256:52ea15fc337547b13d06069c23479c293e23074d4e4a6be21253e4bd57ad12be + ports: + - 7181:7181 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install Tinybird CLI + run: curl -fsSL https://tinybird.co/install.sh | sh + - name: Build project + run: tb build + - name: Test project + run: tb test run + - name: Trigger and watch traffic analytics infra Tinybird workflow + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + env: + GH_TOKEN: ${{ secrets.TRAFFIC_ANALYTICS_GITHUB_TOKEN }} + uses: ./.github/actions/dispatch-workflow + with: + repo: TryGhost/traffic-analytics-infra + workflow: tinybird.yml + branch: main + dispatch-inputs: >- + { + "ghost_ref": "${{ github.sha }}", + "caller_run_id": "${{ github.run_id }}", + "run_local_tests": false + } + + job_ghost-cli: + name: Ghost-CLI tests + needs: [job_setup] + if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + submodules: true + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install Ghost-CLI + run: npm install -g ghost-cli@latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - run: node .github/scripts/bump-version.js canary + + - run: pnpm archive + + - run: mv ghost-*.tgz ghost.tgz + working-directory: ghost/core + + - name: Save Ghost CLI Debug Logs + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ghost-cli-debug-logs + path: /home/runner/.ghost/logs/ + + - name: Clean Install + run: | + DIR=$(mktemp -d) + ghost install local -d $DIR --archive $(pwd)/ghost/core/ghost.tgz + + - name: Latest Release + run: | + DIR=$(mktemp -d) + ghost install local -d $DIR + ghost update -d $DIR --archive $(pwd)/ghost/core/ghost.tgz + + - name: Print debug logs + if: failure() + run: | + [ -f ~/.ghost/logs/*.log ] && cat ~/.ghost/logs/*.log + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_build_artifacts: + name: Build & Publish Artifacts + needs: [job_setup] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build server and admin assets + run: | + PKG_VERSION=$(node -p "require('./ghost/core/package.json').version") + SHORT_SHA="${GITHUB_SHA:0:7}" + if [ "${{ github.ref_type }}" != "tag" ]; then + export GHOST_BUILD_VERSION="${PKG_VERSION}+${SHORT_SHA}" + echo "GHOST_BUILD_VERSION=${GHOST_BUILD_VERSION}" >> $GITHUB_ENV + fi + pnpm build:production + + - name: Verify tag matches package.json + if: startsWith(github.ref, 'refs/tags/v') + working-directory: ghost/core + run: | + PKG_VERSION=$(node -p "require('./package.json').version") + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} doesn't match package.json version ${PKG_VERSION}" + exit 1 + fi + + - name: Pack standalone distribution + run: pnpm --filter ghost pack:standalone + + - name: Create npm tarball + if: startsWith(github.ref, 'refs/tags/v') + run: pnpm --filter ghost pack:tarball + + - name: Upload npm tarball + if: startsWith(github.ref, 'refs/tags/v') + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ghost-npm-tarball + path: ghost/core/ghost-*.tgz + retention-days: 7 + if-no-files-found: error + + - name: Prepare Docker build context + run: mv ghost/core/package/ /tmp/ghost-production/ + + - name: Determine push strategy + id: strategy + run: | + # Same-org repos (e.g. TryGhost/Ghost, TryGhost/Ghost-Security) push to GHCR. + # External forks and cross-repo PRs use artifact-based image transfer instead. + USE_ARTIFACT="false" + if [ "${{ github.repository_owner }}" != "TryGhost" ]; then + # External fork — no GHCR push + USE_ARTIFACT="true" + elif [ "${{ github.event_name }}" = "pull_request" ] && \ + [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then + # Cross-repo PR (fork PR into this repo) — no GHCR push + USE_ARTIFACT="true" + fi + + OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + + # Derive GHCR image names from repository name so each repo gets its own namespace + # TryGhost/Ghost → ghost-core / ghost, TryGhost/Ghost-Security → ghost-security-core / ghost-security + REPO_NAME=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') + if [ "$REPO_NAME" = "ghost" ]; then + IMAGE_CORE_NAME="ghcr.io/${OWNER}/ghost-core" + IMAGE_FULL_NAME="ghcr.io/${OWNER}/ghost" + else + IMAGE_CORE_NAME="ghcr.io/${OWNER}/${REPO_NAME}-core" + IMAGE_FULL_NAME="ghcr.io/${OWNER}/${REPO_NAME}" + fi + + # Force push on tag pushes (release images must always be published) + IS_TAG="${{ startsWith(github.ref, 'refs/tags/v') }}" + if [ "$IS_TAG" = "true" ]; then + USE_ARTIFACT="false" + fi + + echo "use-artifact=$USE_ARTIFACT" >> $GITHUB_OUTPUT + echo "should-push=$( [ "$USE_ARTIFACT" = "false" ] && echo "true" || echo "false" )" >> $GITHUB_OUTPUT + echo "owner=$OWNER" >> $GITHUB_OUTPUT + echo "image-core-name=$IMAGE_CORE_NAME" >> $GITHUB_OUTPUT + echo "image-full-name=$IMAGE_FULL_NAME" >> $GITHUB_OUTPUT + echo "image-e2e-name=${IMAGE_FULL_NAME}-e2e" >> $GITHUB_OUTPUT + + - name: Upload admin artifact for CD + id: upload-admin + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: admin-build-cd + path: apps/admin/dist + retention-days: 7 + if-no-files-found: error + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + + - name: Log in to GitHub Container Registry + if: steps.strategy.outputs.should-push == 'true' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta (core) + id: meta-core + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 + with: + images: ${{ steps.strategy.outputs.image-core-name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=semver,pattern=v{{version}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=Ghost Core + org.opencontainers.image.description=Ghost production build (server only, no admin) + org.opencontainers.image.vendor=TryGhost + + - name: Docker meta (full) + id: meta-full + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 + with: + images: ${{ steps.strategy.outputs.image-full-name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=semver,pattern=v{{version}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=Ghost + org.opencontainers.image.description=Ghost production build (server + admin) + org.opencontainers.image.vendor=TryGhost + + - name: Build & push core image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + with: + context: /tmp/ghost-production + file: Dockerfile.production + target: core + build-args: | + NODE_VERSION=${{ env.NODE_VERSION }} + GHOST_BUILD_VERSION=${{ env.GHOST_BUILD_VERSION }} + push: ${{ steps.strategy.outputs.should-push }} + load: ${{ steps.strategy.outputs.should-push == 'false' }} + tags: ${{ steps.meta-core.outputs.tags }} + labels: ${{ steps.meta-core.outputs.labels }} + cache-from: type=registry,ref=${{ steps.strategy.outputs.image-core-name }}:cache-main + cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', steps.strategy.outputs.image-core-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} + + - name: Build & push full image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + with: + context: /tmp/ghost-production + file: Dockerfile.production + target: full + build-args: | + NODE_VERSION=${{ env.NODE_VERSION }} + GHOST_BUILD_VERSION=${{ env.GHOST_BUILD_VERSION }} + push: ${{ steps.strategy.outputs.should-push }} + load: true + tags: ${{ steps.meta-full.outputs.tags }} + labels: ${{ steps.meta-full.outputs.labels }} + cache-from: type=registry,ref=${{ steps.strategy.outputs.image-full-name }}:cache-main + cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', steps.strategy.outputs.image-full-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} + + - name: Save full image as artifact + run: | + IMAGE_TAG=$(echo "${{ steps.meta-full.outputs.tags }}" | head -n1) + echo "Saving image: $IMAGE_TAG" + docker save "$IMAGE_TAG" | gzip > docker-image-production.tar.gz + echo "Image saved as docker-image-production.tar.gz" + ls -lh docker-image-production.tar.gz + + - name: Upload image artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: docker-image-production + path: docker-image-production.tar.gz + retention-days: 1 + + - name: Inspect image size and layers + shell: bash + run: | + IMAGE_TAG=$(echo "${{ steps.meta-full.outputs.tags }}" | head -n1) + echo "Analyzing Docker image: $IMAGE_TAG" + + # Get the image size in bytes + IMAGE_SIZE_BYTES=$(docker inspect "$IMAGE_TAG" --format='{{.Size}}') + + # Convert to human readable format + IMAGE_SIZE_MB=$(( IMAGE_SIZE_BYTES / 1024 / 1024 )) + IMAGE_SIZE_GB=$(echo "scale=2; $IMAGE_SIZE_BYTES / 1024 / 1024 / 1024" | bc) + + # Format size display based on magnitude + if [ $IMAGE_SIZE_MB -ge 1024 ]; then + IMAGE_SIZE_DISPLAY="${IMAGE_SIZE_GB} GB" + else + IMAGE_SIZE_DISPLAY="${IMAGE_SIZE_MB} MB" + fi + + echo "Image size: ${IMAGE_SIZE_DISPLAY}" + + # Write to GitHub Step Summary + { + echo "# Docker Image Analysis" + echo "" + echo "**Image:** \`$IMAGE_TAG\`" + echo "" + echo "**Total Size:** ${IMAGE_SIZE_DISPLAY}" + echo "" + echo "## Image Layers" + echo "" + echo "| Size | Layer |" + echo "|------|-------|" + + # Get all layers (including 0B ones) + docker history "$IMAGE_TAG" --format "{{.Size}}@@@{{.CreatedBy}}" --no-trunc | \ + while IFS='@@@' read -r size cmd; do + # Clean up the command for display + cmd_clean=$(echo "$cmd" | sed 's/^\/bin\/sh -c //' | sed 's/^#(nop) //' | sed 's/^@@//' | sed 's/|/\\|/g' | cut -c1-80) + if [ ${#cmd} -gt 80 ]; then + cmd_clean="${cmd_clean}..." + fi + echo "| $size | \`${cmd_clean}\` |" + done + + } >> $GITHUB_STEP_SUMMARY + + outputs: + image-tags: ${{ steps.meta-full.outputs.tags }} + use-artifact: ${{ steps.strategy.outputs.use-artifact }} + admin-artifact-id: ${{ steps.upload-admin.outputs.artifact-id }} + image-e2e-name: ${{ steps.strategy.outputs.image-e2e-name }} + + job_build_e2e_public_apps: + name: Build E2E Public App Assets + needs: [job_setup] + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build public apps for E2E + run: pnpm --filter @tryghost/e2e build:apps + + - name: Pack public app artifacts + run: | + tar -czf e2e-public-apps.tar.gz \ + apps/portal/umd \ + apps/comments-ui/umd \ + apps/sodo-search/umd \ + apps/signup-form/umd \ + apps/announcement-bar/umd + ls -lh e2e-public-apps.tar.gz + + - name: Upload public app artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: e2e-public-apps + path: e2e-public-apps.tar.gz + retention-days: 1 + + job_build_e2e_image: + name: Build E2E Docker Image + needs: [job_setup, job_build_e2e_public_apps, job_build_artifacts] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download public app artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: e2e-public-apps + + - name: Extract public app artifacts + run: tar -xzf e2e-public-apps.tar.gz + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + with: + # Fork/cross-repo PRs use artifact transfer (no GHCR push). The default + # docker-container driver runs in an isolated BuildKit container that + # cannot see locally loaded images, so we fall back to the docker driver + # which shares the host daemon's image store. + driver: ${{ needs.job_build_artifacts.outputs.use-artifact == 'true' && 'docker' || '' }} + + - name: Load base Ghost image + uses: ./.github/actions/load-docker-image + id: load-base + with: + use-artifact: ${{ needs.job_build_artifacts.outputs.use-artifact }} + image-tags: ${{ needs.job_build_artifacts.outputs.image-tags }} + artifact-name: docker-image-production + + - name: Determine E2E image distribution strategy + id: strategy + run: | + USE_ARTIFACT="${{ needs.job_build_artifacts.outputs.use-artifact }}" + SHOULD_PUSH="true" + if [ "$USE_ARTIFACT" = "true" ]; then + SHOULD_PUSH="false" + fi + + echo "use-artifact=$USE_ARTIFACT" >> $GITHUB_OUTPUT + echo "should-push=$SHOULD_PUSH" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + if: steps.strategy.outputs.should-push == 'true' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta (e2e) + id: meta-e2e + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 + with: + images: ${{ needs.job_build_artifacts.outputs.image-e2e-name }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=Ghost E2E + org.opencontainers.image.description=Ghost production build with public E2E app bundles + org.opencontainers.image.vendor=TryGhost + + - name: Build & push E2E image + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + with: + context: . + file: e2e/Dockerfile.e2e + build-args: | + GHOST_IMAGE=${{ steps.load-base.outputs.image-tag }} + push: ${{ steps.strategy.outputs.should-push }} + load: ${{ steps.strategy.outputs.use-artifact == 'true' }} + tags: ${{ steps.meta-e2e.outputs.tags }} + labels: ${{ steps.meta-e2e.outputs.labels }} + cache-from: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-main', needs.job_build_artifacts.outputs.image-e2e-name) || '' }} + cache-to: ${{ steps.strategy.outputs.should-push == 'true' && format('type=registry,ref={0}:cache-{1},mode=max', needs.job_build_artifacts.outputs.image-e2e-name, github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'main') || '' }} + + - name: Save E2E image as artifact + if: steps.strategy.outputs.use-artifact == 'true' + run: | + IMAGE_TAG=$(echo "${{ steps.meta-e2e.outputs.tags }}" | head -n1) + echo "Saving image: $IMAGE_TAG" + docker save "$IMAGE_TAG" | gzip > docker-image-e2e.tar.gz + echo "Image saved as docker-image-e2e.tar.gz" + ls -lh docker-image-e2e.tar.gz + + - name: Upload E2E image artifact + if: steps.strategy.outputs.use-artifact == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: docker-image-e2e + path: docker-image-e2e.tar.gz + retention-days: 1 + + outputs: + image-tags: ${{ steps.meta-e2e.outputs.tags }} + use-artifact: ${{ steps.strategy.outputs.use-artifact }} + + job_e2e_tests: + name: E2E Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) + runs-on: ubuntu-latest + needs: [job_build_e2e_image, job_setup] + strategy: + fail-fast: true + matrix: + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + + - name: Pull or build Tinybird CLI Image + run: | + COMPOSE_IMAGE="${COMPOSE_PROJECT_NAME:-ghost-dev}-tb-cli" + # Try pulling pre-built image from GHCR first (fast path) + if docker pull ghcr.io/tryghost/tb-cli:latest 2>/dev/null; then + echo "Pulled tb-cli from GHCR" + docker tag ghcr.io/tryghost/tb-cli:latest "$COMPOSE_IMAGE" + else + echo "GHCR image not available, building from source" + docker buildx build --load -t "$COMPOSE_IMAGE" -f docker/tb-cli/Dockerfile . + fi + + - name: Load Image + uses: ./.github/actions/load-docker-image + id: load + with: + use-artifact: ${{ needs.job_build_e2e_image.outputs.use-artifact }} + image-tags: ${{ needs.job_build_e2e_image.outputs.image-tags }} + artifact-name: docker-image-e2e + + - name: Setup Docker Registry Mirrors + uses: ./.github/actions/setup-docker-registry-mirrors + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Prepare E2E CI job + env: + GHOST_E2E_IMAGE: ${{ steps.load.outputs.image-tag }} + GHOST_E2E_SKIP_IMAGE_BUILD: 'true' + run: bash ./e2e/scripts/prepare-ci-e2e-job.sh + + - name: Run e2e tests in Playwright container + env: + TEST_WORKERS_COUNT: 1 + GHOST_E2E_MODE: build + GHOST_E2E_IMAGE: ${{ steps.load.outputs.image-tag }} + E2E_SHARD_INDEX: ${{ matrix.shardIndex }} + E2E_SHARD_TOTAL: ${{ matrix.shardTotal }} + E2E_RETRIES: 2 + run: bash ./e2e/scripts/run-playwright-container.sh + + - name: Dump E2E docker logs + if: failure() + run: bash ./e2e/scripts/dump-e2e-docker-logs.sh + + - name: Stop E2E infra + if: always() + run: pnpm --filter @tryghost/e2e infra:down + + - name: Upload blob report to GitHub Actions Artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: blob-report-${{ matrix.shardIndex }} + path: e2e/blob-report + retention-days: 1 + + - name: Upload test results artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: test-results-${{ matrix.shardIndex }} + path: e2e/test-results + retention-days: 7 + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + job_merge_e2e_reports: + name: Merge Reports + if: always() + needs: [job_e2e_tests, job_setup] + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + continue-on-error: true + with: + path: e2e/all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Check for blob reports + id: check + run: | + if [ -d "e2e/all-blob-reports" ] && [ -n "$(ls -A e2e/all-blob-reports 2>/dev/null)" ]; then + echo "has_reports=true" >> $GITHUB_OUTPUT + else + echo "has_reports=false" >> $GITHUB_OUTPUT + fi + + - name: Download test results from GitHub Actions Artifacts + if: steps.check.outputs.has_reports == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + path: e2e/all-test-results + pattern: test-results-* + merge-multiple: true + + - name: Merge into HTML Report + if: steps.check.outputs.has_reports == 'true' + run: npx playwright merge-reports --reporter html ./all-blob-reports + working-directory: e2e + + - name: Upload HTML report + if: steps.check.outputs.has_reports == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: playwright-report + path: e2e/playwright-report + retention-days: 14 + + - name: Upload merged test results + if: steps.check.outputs.has_reports == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: test-results + path: e2e/all-test-results + retention-days: 7 + + - name: View Test Report command + if: steps.check.outputs.has_reports == 'true' + run: | + echo -e "::notice::To view the Playwright report locally, run:\n\nREPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\"" + + - name: Comment on PR with test report command + if: github.event_name == 'pull_request' && steps.check.outputs.has_reports == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr comment ${{ github.event.pull_request.number }} --body "## E2E Tests Failed + + To view the Playwright test report locally, run: + + \`\`\`bash + REPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\" + \`\`\`" + + job_coverage: + name: Coverage + needs: [ + job_admin-tests, + job_acceptance-tests, + job_unit-tests + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore Admin coverage + if: contains(needs.job_admin-tests.result, 'success') + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: admin-coverage + + - name: Move coverage + if: contains(needs.job_admin-tests.result, 'success') + run: | + rsync -av --remove-source-files admin/* ghost/admin + + - name: Upload Admin test coverage + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 + with: + flags: admin-tests + + - name: Restore E2E coverage + if: contains(needs.job_acceptance-tests.result, 'success') + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: e2e-coverage + + - name: Move coverage + if: contains(needs.job_acceptance-tests.result, 'success') + run: | + rsync -av --remove-source-files core/* ghost/core + + - name: Upload E2E test coverage + if: contains(needs.job_acceptance-tests.result, 'success') + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 + with: + flags: e2e-tests + + job_required_tests: + name: All required tests passed or skipped + needs: + [ + job_setup, + job_app_version_bump_check, + job_lint, + job_i18n, + job_ghost-cli, + job_admin-tests, + job_unit-tests, + job_acceptance-tests, + job_legacy-tests, + job_admin_x_settings, + job_activitypub, + job_comments_ui, + job_signup_form, + job_tinybird-tests, + job_build_e2e_public_apps, + job_build_e2e_image, + job_e2e_tests, + publish_packages + ] + if: always() + runs-on: ubuntu-latest + steps: + - name: Output needs + run: echo "${{ toJson(needs) }}" + + - name: Check if any required jobs failed or been cancelled + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + echo "One of the dependent jobs have failed or been cancelled. You may need to re-run it." && exit 1 + + publish_packages: + needs: [ + job_setup, + job_lint, + job_unit-tests + ] + name: Publish ${{ matrix.package_name }} + runs-on: ubuntu-latest + if: always() && github.repository == 'TryGhost/Ghost' && needs.job_setup.result == 'success' && needs.job_lint.result == 'success' && needs.job_unit-tests.result == 'success' + permissions: + id-token: write + strategy: + matrix: + include: + - package_name: '@tryghost/activitypub' + package_path: 'apps/activitypub' + cdn_paths: 'https://cdn.jsdelivr.net/ghost/activitypub@CURRENT_MAJOR/dist/activitypub.js' + - package_name: '@tryghost/portal' + package_path: 'apps/portal' + cdn_paths: 'https://cdn.jsdelivr.net/ghost/portal@~CURRENT_MINOR/umd/portal.min.js' + - package_name: '@tryghost/sodo-search' + package_path: 'apps/sodo-search' + cdn_paths: | + https://cdn.jsdelivr.net/ghost/sodo-search@~CURRENT_MINOR/umd/sodo-search.min.js + https://cdn.jsdelivr.net/ghost/sodo-search@~CURRENT_MINOR/umd/main.css + - package_name: '@tryghost/comments-ui' + package_path: 'apps/comments-ui' + cdn_paths: 'https://cdn.jsdelivr.net/ghost/comments-ui@~CURRENT_MINOR/umd/comments-ui.min.js' + - package_name: '@tryghost/signup-form' + package_path: 'apps/signup-form' + cdn_paths: 'https://cdn.jsdelivr.net/ghost/signup-form@~CURRENT_MINOR/umd/signup-form.min.js' + - package_name: '@tryghost/announcement-bar' + package_path: 'apps/announcement-bar' + cdn_paths: 'https://cdn.jsdelivr.net/ghost/announcement-bar@~CURRENT_MINOR/umd/announcement-bar.min.js' + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check if version changed + if: needs.job_setup.outputs.is_main == 'true' + id: version_check + working-directory: ${{ matrix.package_path }} + run: | + CURRENT_VERSION=$(cat package.json | jq -r .version) + echo "Current version: $CURRENT_VERSION" + + CURRENT_MINOR=$(cat package.json | jq -r .version | awk -F. '{print $1"."$2}') + echo "current_minor=$CURRENT_MINOR" >> $GITHUB_OUTPUT + + CURRENT_MAJOR=$(cat package.json | jq -r .version | awk -F. '{print $1}') + echo "current_major=$CURRENT_MAJOR" >> $GITHUB_OUTPUT + + PUBLISHED_VERSION=$(npm show ${{ matrix.package_name }} version || echo "0.0.0") + echo "Published version (latest): $PUBLISHED_VERSION" + + if [ "$CURRENT_VERSION" = "$PUBLISHED_VERSION" ]; then + echo "Version is unchanged." + echo "version_changed=false" >> $GITHUB_OUTPUT + else + echo "Version has changed." + echo "version_changed=true" >> $GITHUB_OUTPUT + fi + + - name: Build the package + if: steps.version_check.outputs.version_changed == 'true' || github.event_name == 'pull_request' + run: pnpm nx build ${{ matrix.package_name }} + + - name: Configure .npmrc + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + run: | + echo "@tryghost:registry=https://registry.npmjs.org/" >> ~/.npmrc + + # TODO: Check we can remove this once we update Node to v24 + - name: Install v11 of NPM # We need this to install packages via OIDC. + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + run: npm install -g npm@11 + + - name: Publish to npm + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + working-directory: ${{ matrix.package_path }} + run: | + npm publish --access public + + - name: Replace version placeholders in cdn-paths + id: cdn_paths + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + run: | + cdn_paths="${{ matrix.cdn_paths }}" + echo "cdn_paths<> $GITHUB_OUTPUT + echo "$cdn_paths" | sed -e 's/CURRENT_MINOR/${{ steps.version_check.outputs.current_minor }}/g' -e 's/CURRENT_MAJOR/${{ steps.version_check.outputs.current_major }}/g' >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Print cdn_paths + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + run: echo "${{ steps.cdn_paths.outputs.cdn_paths }}" + + - name: Wait before purging jsDelivr cache + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' && matrix.package_name == '@tryghost/activitypub' + run: | + echo "Purging jsDelivr cache immediately after publishing a new version on NPM is unreliable. Waiting 1 minute before purging cache..." + sleep 60 + + - name: Purge jsDelivr cache + if: needs.job_setup.outputs.is_main == 'true' && steps.version_check.outputs.version_changed == 'true' + uses: gacts/purge-jsdelivr-cache@8d92aea944f1a3e8ad70505379e1a8ac72d56b73 # v1 + with: + url: ${{ steps.cdn_paths.outputs.cdn_paths }} + + deploy_tinybird: + name: Deploy Tinybird + runs-on: ubuntu-latest + needs: [ + job_setup, + job_tinybird-tests + ] + if: always() && github.repository == 'TryGhost/Ghost' && github.event_name == 'push' && needs.job_setup.outputs.changed_tinybird_datafiles == 'true' && needs.job_setup.result == 'success' && needs.job_tinybird-tests.result == 'success' && needs.job_setup.outputs.is_main == 'true' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: Trigger and watch traffic analytics infra Tinybird workflow + if: github.repository == 'TryGhost/Ghost' + env: + GH_TOKEN: ${{ secrets.TRAFFIC_ANALYTICS_GITHUB_TOKEN }} + uses: ./.github/actions/dispatch-workflow + with: + repo: TryGhost/traffic-analytics-infra + workflow: tinybird.yml + branch: main + dispatch-inputs: >- + { + "ghost_ref": "${{ github.sha }}", + "caller_run_id": "${{ github.run_id }}", + "run_local_tests": false, + "deploy_staging": true, + "deploy_production": true + } + + # --------------------------------------------------------------------------- # + # Trigger Pro CD — dispatch to Ghost-Moya cd.yml (runs on main + PRs) + # --------------------------------------------------------------------------- # + trigger_cd: + needs: [job_setup, job_build_artifacts] + name: Trigger Pro CD + runs-on: ubuntu-latest + if: | + always() + && github.repository == 'TryGhost/Ghost' + && needs.job_setup.result == 'success' + && needs.job_build_artifacts.result == 'success' + && needs.job_build_artifacts.outputs.use-artifact != 'true' + steps: + - name: Determine dispatch parameters + id: params + run: | + if [ "${{ needs.job_setup.outputs.is_main }}" = "true" ]; then + echo "pr_number=" >> $GITHUB_OUTPUT + echo "deploy=" >> $GITHUB_OUTPUT + elif [ "${{ needs.job_setup.outputs.is_tag }}" = "true" ]; then + echo "pr_number=" >> $GITHUB_OUTPUT + echo "deploy=" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "pull_request" ]; then + echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + + # DISABLED: deploy-to-staging label detection is disabled. + # The label workflow has fundamental problems — admin deploys are global + # (not per-site) and main merges overwrite the deployment immediately. + # See deploy-to-staging.yml for details. + echo "deploy=" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + echo "skip=false" >> $GITHUB_OUTPUT + + - name: Dispatch to Ghost-Moya cd.yml + if: steps.params.outputs.skip != 'true' + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 + with: + token: ${{ secrets.CANARY_DOCKER_BUILD }} + repository: TryGhost/Ghost-Moya + event-type: ghost-artifacts-ready + client-payload: >- + { + "ref": "${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || github.sha }}", + "source_repo": "${{ github.repository }}", + "pr_number": "${{ steps.params.outputs.pr_number }}", + "deploy": "${{ steps.params.outputs.deploy }}", + "admin_artifact_id": "${{ needs.job_build_artifacts.outputs.admin-artifact-id }}", + "admin_artifact_run_id": "${{ github.run_id }}" + } + + # --------------------------------------------------------------------------- # + # Publish Ghost npm package — runs on version tags only (OIDC, no stored token) + # --------------------------------------------------------------------------- # + publish_ghost: + needs: [job_setup, job_build_artifacts] + name: Publish Ghost to npm + runs-on: ubuntu-latest + if: | + startsWith(github.ref, 'refs/tags/v') + && github.repository == 'TryGhost/Ghost' + && needs.job_build_artifacts.result == 'success' + environment: npm-release + permissions: + id-token: write + steps: + - name: Download npm tarball + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ghost-npm-tarball + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + package-manager-cache: false + + # TODO: Remove once Node v24 ships with npm >= 11 + - name: Install npm v11 (required for OIDC publishing) + run: npm install -g npm@11 + + - name: Verify tarball contents + run: | + echo "Tarball contents:" + tar -tzf ghost-*.tgz | head -20 + tar -tzf ghost-*.tgz | grep -q 'package/.npmrc' || { echo "::error::.npmrc not found in tarball"; exit 1; } + tar -tzf ghost-*.tgz | grep -q 'package/pnpm-lock.yaml' || { echo "::error::pnpm-lock.yaml not found in tarball"; exit 1; } + tar -xOf ghost-*.tgz package/package.json | jq -e '.packageManager' >/dev/null || { echo "::error::packageManager not found in packaged package.json"; exit 1; } + + - name: Publish to npm + run: npm publish ghost-*.tgz --access public + + - uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + if: failure() + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + # --------------------------------------------------------------------------- # + # Create GitHub Release — runs after successful npm publish + # --------------------------------------------------------------------------- # + create_github_release: + needs: [publish_ghost] + name: Create GitHub Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + env: + GH_TOKEN: ${{ secrets.CANARY_DOCKER_BUILD }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Resolve previous tag + id: prev_tag + run: | + CURRENT_TAG="${GITHUB_REF_NAME}" + # Find the tag immediately before this one (excluding pre-releases) + PREV_TAG=$(git tag --list 'v[0-9]*' --sort=-version:refname | grep -v '-' | grep -v "^${CURRENT_TAG}$" | head -n 1) + if [ -z "$PREV_TAG" ]; then + echo "::warning::No previous stable tag found — release notes will use fallback message" + fi + echo "tag=${PREV_TAG}" >> "$GITHUB_OUTPUT" + echo "Previous tag: ${PREV_TAG:-}" + + - name: Generate release notes + id: notes + run: | + PREV_TAG="${{ steps.prev_tag.outputs.tag }}" + if [ -n "$PREV_TAG" ]; then + node scripts/lib/release-notes.js "$PREV_TAG" "${GITHUB_REF_NAME}" > /tmp/release-notes.md + else + echo "This release contains fixes for minor bugs and issues reported by Ghost users." > /tmp/release-notes.md + fi + cat /tmp/release-notes.md + + - name: Create GitHub Release + run: | + gh release create "${GITHUB_REF_NAME}" \ + --title "${GITHUB_REF_NAME}" \ + --notes-file /tmp/release-notes.md + + - name: Notify Slack + if: always() && steps.notes.outcome == 'success' + run: | + VERSION="${GITHUB_REF_NAME}" + RELEASE_URL="https://github.com/TryGhost/Ghost/releases/tag/${VERSION}" + CHANGELOG=$(cat /tmp/release-notes.md | head -c 3000) + + # Build Slack payload — use --rawfile so newlines in release notes are preserved + PAYLOAD=$(jq -n \ + --arg header ":ghost: Ghost ${VERSION} is loose! - ${RELEASE_URL}" \ + --rawfile notes /tmp/release-notes.md \ + '{text: ($header + "\n\n" + $notes)}') + + curl -sf -X POST \ + -H 'Content-type: application/json' \ + --data "${PAYLOAD}" \ + "${{ secrets.RELEASE_NOTIFICATION_URL }}" || echo "Slack notification failed (non-fatal)" diff --git a/.github/workflows/cleanup-ghcr.yml b/.github/workflows/cleanup-ghcr.yml new file mode 100644 index 0000000..5060aa2 --- /dev/null +++ b/.github/workflows/cleanup-ghcr.yml @@ -0,0 +1,158 @@ +name: Cleanup GHCR Images + +on: + schedule: + - cron: "30 4 * * *" # Daily at 04:30 UTC + workflow_dispatch: + inputs: + dry_run: + description: "Log what would be deleted without making changes" + required: false + default: true + type: boolean + retention_days: + description: "Delete versions older than this many days" + required: false + default: 14 + type: number + min_keep: + description: "Always keep at least this many versions per package" + required: false + default: 10 + type: number + +permissions: + packages: write + +env: + ORG: TryGhost + RETENTION_DAYS: ${{ inputs.retention_days || 14 }} + MIN_KEEP: ${{ inputs.min_keep || 10 }} + +jobs: + cleanup: + name: Cleanup + runs-on: ubuntu-latest + strategy: + matrix: + package: [ghost, ghost-core, ghost-development] + steps: + - name: Delete old non-release versions + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ github.event_name == 'schedule' && 'false' || inputs.dry_run }} + PACKAGE: ${{ matrix.package }} + run: | + set -euo pipefail + + cutoff=$(date -u -d "-${RETENTION_DAYS} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ + || date -u -v-${RETENTION_DAYS}d +%Y-%m-%dT%H:%M:%SZ) + + echo "Package: ${ORG}/${PACKAGE}" + echo "Cutoff: ${cutoff} (${RETENTION_DAYS} days ago)" + echo "Dry run: ${DRY_RUN}" + echo "" + + # Pagination — collect all versions + page=1 + all_versions="[]" + while true; do + if ! batch=$(gh api \ + "/orgs/${ORG}/packages/container/${PACKAGE}/versions?per_page=100&page=${page}" \ + --jq '.' 2>&1); then + if [ "$page" = "1" ]; then + echo "::error::API request failed: ${batch}" + exit 1 + fi + echo "::warning::API request failed (page ${page}): ${batch}" + break + fi + + count=$(echo "$batch" | jq 'length') + if [ "$count" = "0" ]; then + break + fi + + all_versions=$(echo "$all_versions $batch" | jq -s 'add') + page=$((page + 1)) + done + + total=$(echo "$all_versions" | jq 'length') + echo "Total versions: ${total}" + + # Classify versions + keep=0 + delete=0 + delete_ids="" + + for row in $(echo "$all_versions" | jq -r '.[] | @base64'); do + _jq() { echo "$row" | base64 -d | jq -r "$1"; } + + id=$(_jq '.id') + updated=$(_jq '.updated_at') + tags=$(_jq '[.metadata.container.tags[]] | join(",")') + + # Keep versions with semver tags (v1.2.3, 1.2.3, 1.2) + if echo "$tags" | grep -qE '(^|,)v?[0-9]+\.[0-9]+\.[0-9]+(,|$)' || \ + echo "$tags" | grep -qE '(^|,)[0-9]+\.[0-9]+(,|$)'; then + keep=$((keep + 1)) + continue + fi + + # Keep versions with 'latest' or 'main' or cache-main tags + if echo "$tags" | grep -qE '(^|,)(latest|main|cache-main)(,|$)'; then + keep=$((keep + 1)) + continue + fi + + # Keep versions newer than cutoff + if [[ "$updated" > "$cutoff" ]]; then + keep=$((keep + 1)) + continue + fi + + # This version is eligible for deletion + delete=$((delete + 1)) + delete_ids="${delete_ids} ${id}" + + tag_display="${tags:-}" + if [ "$DRY_RUN" = "true" ]; then + echo "[dry-run] Would delete version ${id} (tags: ${tag_display}, updated: ${updated})" + fi + done + + echo "" + echo "Summary: ${keep} kept, ${delete} to delete (of ${total} total)" + + if [ "$delete" = "0" ]; then + echo "Nothing to delete." + exit 0 + fi + + # Safety check — run before dry-run exit so users see the warning + if [ "$keep" -lt "$MIN_KEEP" ]; then + echo "::error::Safety check failed — only ${keep} versions would remain (minimum: ${MIN_KEEP}). Aborting." + exit 1 + fi + + if [ "$DRY_RUN" = "true" ]; then + echo "" + echo "Dry run — no versions deleted." + exit 0 + fi + + # Delete eligible versions + deleted=0 + failed=0 + for id in $delete_ids; do + if gh api --method DELETE \ + "/orgs/${ORG}/packages/container/${PACKAGE}/versions/${id}" 2>/dev/null; then + deleted=$((deleted + 1)) + else + echo "::warning::Failed to delete version ${id}" + failed=$((failed + 1)) + fi + done + + echo "" + echo "Deleted ${deleted} versions (${failed} failed)" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000..84d424b --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install gh-aw extension + uses: github/gh-aw/actions/setup-cli@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + version: v0.49.3 diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml new file mode 100644 index 0000000..2fce9e3 --- /dev/null +++ b/.github/workflows/create-release-branch.yml @@ -0,0 +1,66 @@ +name: Create release branch +on: + workflow_dispatch: + inputs: + base-ref: + description: 'Git ref to base from (defaults to latest tag)' + type: string + default: 'latest' + required: false + bump-type: + description: 'Version bump type (patch, minor)' + type: string + required: false + default: 'patch' +env: + FORCE_COLOR: 1 +permissions: + contents: write +jobs: + create-branch: + if: github.repository == 'TryGhost/Ghost' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: inputs.base-ref == 'latest' + with: + ref: main + fetch-depth: 0 + submodules: true + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: inputs.base-ref != 'latest' + with: + ref: ${{ inputs.base-ref }} + fetch-depth: 0 + submodules: true + + - name: Checkout most recent tag + run: git checkout "$(git describe --tags --abbrev=0 --match=v*)" + if: inputs.base-ref == 'latest' + + - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4 + with: + tool_versions: | + semver 3.3.0 + + - run: | + CURRENT_TAG=$(git describe --tags --abbrev=0 --match=v*) + NEW_VERSION=$(semver bump "$BUMP_TYPE_INPUT" "$CURRENT_TAG") + printf 'CURRENT_SHA=%s\n' "$(git rev-parse HEAD)" >> "$GITHUB_ENV" + printf 'NEW_VERSION=%s\n' "$NEW_VERSION" >> "$GITHUB_ENV" + env: + BUMP_TYPE_INPUT: ${{ inputs.bump-type }} + + - name: Create branch + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const branchName = `v${process.env.NEW_VERSION}`; + console.log(`Creating branch: ${branchName}`); + await github.request('POST /repos/{owner}/{repo}/git/refs', { + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/heads/${branchName}`, + sha: process.env.CURRENT_SHA + }); diff --git a/.github/workflows/deploy-to-staging.yml b/.github/workflows/deploy-to-staging.yml new file mode 100644 index 0000000..cc8c0ec --- /dev/null +++ b/.github/workflows/deploy-to-staging.yml @@ -0,0 +1,127 @@ +name: Deploy to Staging + +# DISABLED: The deploy-to-staging label workflow is currently broken and disabled. +# Problems: +# 1. Admin is global — deploying a PR's admin overwrites admin-forward/ for ALL staging +# sites, not just demo.ghost.is. Per-site admin versioning is needed first. +# 2. Main merges overwrite — any merge to main triggers a full staging rollout that +# overwrites both the server version on demo.ghost.is and admin-forward/ globally. +# The deployment lasts only until the next merge to main, making it unreliable. +# See: https://www.notion.so/ghost/Proposal-Per-site-admin-versioning-31951439c03081daa133eb0215642202 + +on: + pull_request_target: + types: [labeled] + +jobs: + deploy: + name: Deploy to Staging + # Runs when the "deploy-to-staging" label is added — requires collaborator write access. + # Fork PRs are rejected because they don't have GHCR images (CI uses artifact transfer). + if: >- + false + && github.event.label.name == 'deploy-to-staging' + && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + steps: + - name: Wait for CI build artifacts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Waiting for CI to complete Docker build for $HEAD_SHA..." + TIMEOUT=1800 # 30 minutes + INTERVAL=30 + START=$(date +%s) + + while true; do + ELAPSED=$(( $(date +%s) - START )) + if [ "$ELAPSED" -ge "$TIMEOUT" ]; then + echo "::error::Timed out waiting for CI (${TIMEOUT}s)" + exit 1 + fi + + # Find the CI run for this SHA + RUN=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${HEAD_SHA}&per_page=1" \ + --jq '.workflow_runs[0] | {id, status, conclusion}' 2>/dev/null || echo "") + + if [ -z "$RUN" ] || [ "$RUN" = "null" ]; then + echo " No CI run found yet, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + continue + fi + + STATUS=$(echo "$RUN" | jq -r '.status') + CONCLUSION=$(echo "$RUN" | jq -r '.conclusion // empty') + RUN_ID=$(echo "$RUN" | jq -r '.id') + + if [ "$STATUS" = "completed" ]; then + if [ "$CONCLUSION" = "success" ] || [ "$CONCLUSION" = "failure" ]; then + # Check if Docker build job specifically succeeded (paginate — CI has 30+ jobs) + BUILD_JOB=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + --jq '.jobs[] | select(.name == "Build & Publish Artifacts") | .conclusion') + if [ -z "$BUILD_JOB" ]; then + echo "::error::Build & Publish Artifacts job not found in CI run ${RUN_ID}" + exit 1 + elif [ "$BUILD_JOB" = "success" ]; then + echo "Docker build ready (CI run $RUN_ID)" + break + else + echo "::error::Docker build job did not succeed (conclusion: $BUILD_JOB)" + exit 1 + fi + else + echo "::error::CI run failed (conclusion: $CONCLUSION)" + exit 1 + fi + fi + + echo " CI still running ($STATUS), waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + done + + - name: Re-check PR eligibility + id: recheck + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR=$(gh api "repos/${{ github.repository }}/pulls/${{ env.PR_NUMBER }}" \ + --jq '{state, labels: [.labels[].name], head_sha: .head.sha}') + + STATE=$(echo "$PR" | jq -r '.state') + HAS_LABEL=$(echo "$PR" | jq '.labels | any(. == "deploy-to-staging")') + CURRENT_SHA=$(echo "$PR" | jq -r '.head_sha') + + if [ "$STATE" != "open" ]; then + echo "::warning::PR is no longer open ($STATE), skipping dispatch" + echo "skip=true" >> "$GITHUB_OUTPUT" + elif [ "$HAS_LABEL" != "true" ]; then + echo "::warning::deploy-to-staging label was removed, skipping dispatch" + echo "skip=true" >> "$GITHUB_OUTPUT" + elif [ "$CURRENT_SHA" != "$HEAD_SHA" ]; then + echo "::warning::HEAD SHA changed ($HEAD_SHA → $CURRENT_SHA), skipping dispatch (new push will trigger CI)" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "PR still eligible for deploy" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Dispatch to Ghost-Moya + if: steps.recheck.outputs.skip != 'true' + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 + with: + token: ${{ secrets.CANARY_DOCKER_BUILD }} + repository: TryGhost/Ghost-Moya + event-type: ghost-artifacts-ready + client-payload: >- + { + "ref": "${{ env.PR_NUMBER }}", + "source_repo": "${{ github.repository }}", + "pr_number": "${{ env.PR_NUMBER }}", + "deploy": "true" + } diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml new file mode 100644 index 0000000..3ed6b2f --- /dev/null +++ b/.github/workflows/label-actions.yml @@ -0,0 +1,21 @@ +name: 'Label Issues & PRs' + +on: + workflow_dispatch: + issues: + types: [opened, closed, labeled] + pull_request_target: + types: [opened, closed, labeled] + schedule: + - cron: '0 * * * *' + +permissions: + issues: write + pull-requests: write + +jobs: + action: + runs-on: ubuntu-latest + if: github.repository_owner == 'TryGhost' + steps: + - uses: tryghost/actions/actions/label-actions@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main diff --git a/.github/workflows/linear-triage.lock.yml b/.github/workflows/linear-triage.lock.yml new file mode 100644 index 0000000..b88d6c6 --- /dev/null +++ b/.github/workflows/linear-triage.lock.yml @@ -0,0 +1,1141 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.51.5). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Triage new Linear issues for the Berlin Bureau (BER) team — classify type, assign priority, tag product area, and post reasoning comments. +# +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"c42f30d0477c02ecc7ccee1daa2da631c7f63302d41e2d4beb79839fcddcaca2","compiler_version":"v0.51.5"} + +name: "Linear Issue Triage Agent" +"on": + schedule: + - cron: "17 16 * * 1-5" + # Friendly format: daily on weekdays (scattered) + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Linear Issue Triage Agent" + +jobs: + activation: + if: github.repository == 'TryGhost/Ghost' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + destination: /opt/gh-aw/actions + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_INFO_VERSION: "" + GH_AW_INFO_AGENT_VERSION: "0.0.420" + GH_AW_INFO_CLI_VERSION: "v0.51.5" + GH_AW_INFO_WORKFLOW_NAME: "Linear Issue Triage Agent" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","node","mcp.linear.app"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "linear-triage.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + { + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" + cat "/opt/gh-aw/prompts/markdown.md" + cat "/opt/gh-aw/prompts/cache_memory_prompt.md" + cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_EOF' + + Tools: create_issue, missing_tool, missing_data, noop + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' + {{#runtime-import .github/workflows/linear-triage.md}} + GH_AW_PROMPT_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: lineartriage + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ needs.activation.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: '24' + package-manager-cache: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash /opt/gh-aw/actions/create_cache_memory_dir.sh + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + (github.event.pull_request) || (github.event.issue.pull_request) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.420 + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.6 ghcr.io/github/github-mcp-server:v0.31.0 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "parent": { + "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from a previously created issue in the same workflow run.", + "type": [ + "number", + "string" + ] + }, + "temporary_id": { + "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 3 to 8 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", + "pattern": "^aw_[A-Za-z0-9]{3,8}$", + "type": "string" + }, + "title": { + "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_issue" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e LINEAR_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.6' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.31.0", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "linear": { + "type": "stdio", + "container": "node:lts-alpine", + "entrypoint": "npx", + "entrypointArgs": [ + "npx", + "-y", + "mcp-remote", + "https://mcp.linear.app/mcp", + "--header", + "Authorization:Bearer \${LINEAR_API_KEY}" + ], + "tools": [ + "*" + ], + "env": { + "LINEAR_API_KEY": "\${LINEAR_API_KEY}" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,mcp.linear.app,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_MODEL_AGENT_COPILOT: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: | + # Copy Copilot session state files to logs folder for artifact collection + # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them + SESSION_STATE_DIR="$HOME/.copilot/session-state" + LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" + + if [ -d "$SESSION_STATE_DIR" ]; then + echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" + mkdir -p "$LOGS_DIR" + cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true + echo "Session state files copied successfully" + else + echo "No session-state directory found at $SESSION_STATE_DIR" + fi + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,LINEAR_API_KEY' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,mcp.linear.app,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Upload engine output files + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: agent_outputs + path: | + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + if-no-files-found: ignore + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + # --- Threat Detection (inline) --- + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Linear Issue Triage Agent" + WORKFLOW_DESCRIPTION: "Triage new Linear issues for the Berlin Bureau (BER) team — classify type, assign priority, tag product area, and post reasoning comments." + HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + # --allow-tool shell(cat) + # --allow-tool shell(grep) + # --allow-tool shell(head) + # --allow-tool shell(jq) + # --allow-tool shell(ls) + # --allow-tool shell(tail) + # --allow-tool shell(wc) + timeout-minutes: 20 + run: | + set -o pipefail + # shellcheck disable=SC1003 + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ + -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_DETECTION_COPILOT:+ --model "$GH_AW_MODEL_DETECTION_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_MODEL_DETECTION_COPILOT: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: ${{ env.GITHUB_STEP_SUMMARY }} + GITHUB_WORKSPACE: ${{ github.workspace }} + XDG_CONFIG_HOME: /home/runner + - name: Parse threat detection results + id: parse_detection_results + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Set detection conclusion + id: detection_conclusion + if: always() + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} + run: | + if [[ "$RUN_DETECTION" != "true" ]]; then + echo "conclusion=skipped" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection was not needed, marking as skipped" + elif [[ "$DETECTION_SUCCESS" == "true" ]]; then + echo "conclusion=success" >> "$GITHUB_OUTPUT" + echo "success=true" >> "$GITHUB_OUTPUT" + echo "Detection passed successfully" + else + echo "conclusion=failure" >> "$GITHUB_OUTPUT" + echo "success=false" >> "$GITHUB_OUTPUT" + echo "Detection found issues" + fi + + conclusion: + needs: + - activation + - agent + - safe_outputs + - update_cache_memory + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Linear Issue Triage Agent" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Linear Issue Triage Agent" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Linear Issue Triage Agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "linear-triage" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_GROUP_REPORTS: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Linear Issue Triage Agent" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + + safe_outputs: + needs: agent + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + issues: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "copilot" + GH_AW_WORKFLOW_ID: "linear-triage" + GH_AW_WORKFLOW_NAME: "Linear Issue Triage Agent" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,mcp.linear.app,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"max\":1},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload safe output items manifest + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: safe-output-items + path: /tmp/safe-output-items.jsonl + if-no-files-found: warn + + update_cache_memory: + needs: agent + if: always() && needs.agent.outputs.detection_success == 'true' + runs-on: ubuntu-latest + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: lineartriage + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ce1794953e0ec42adc41b6fca05e02ab49ee21c3 # v0.68.3 + with: + destination: /opt/gh-aw/actions + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/linear-triage.md b/.github/workflows/linear-triage.md new file mode 100644 index 0000000..1550fd8 --- /dev/null +++ b/.github/workflows/linear-triage.md @@ -0,0 +1,232 @@ +--- +description: Triage new Linear issues for the Berlin Bureau (BER) team — classify type, assign priority, tag product area, and post reasoning comments. +on: + workflow_dispatch: + schedule: daily on weekdays +permissions: + contents: read +if: github.repository == 'TryGhost/Ghost' +tools: + cache-memory: true +mcp-servers: + linear: + command: "npx" + args: ["-y", "mcp-remote", "https://mcp.linear.app/mcp", "--header", "Authorization:Bearer ${{ secrets.LINEAR_API_KEY }}"] + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} +network: + allowed: + - defaults + - node + - mcp.linear.app +safe-outputs: + create-issue: + noop: +--- + +# Linear Issue Triage Agent + +You are an AI agent that triages new Linear issues for the **Berlin Bureau (BER)** team. Your goal is to reduce the time a human needs to complete triage by pre-classifying issues, assigning priority, tagging product areas, and recommending code investigations where appropriate. + +**You do not move issues out of Triage** — a human still makes the final call on status transitions. + +## Your Task + +1. Use the Linear MCP tools to find the BER team and list all issues currently in the **Triage** state +2. Check your cache-memory to see which issues you have already triaged — skip those +3. For each untriaged issue, apply the triage rubric below to: + - Classify the issue type + - Assign priority (both a priority label and Linear's built-in priority field) + - Tag the product area + - Post a triage comment explaining your reasoning +4. Update your cache-memory with the newly triaged issue IDs +5. After processing, call the `noop` safe output with a summary of what you did — e.g. "Triaged 1 issue: BER-3367 (Bug, P3)" or "No new BER issues in Triage state" if there was nothing to triage + +## Linear MCP Tools + +You have access to the official Linear MCP server. Use its tools to: + +- **Find issues**: Search for BER team issues in Triage state +- **Read issue details**: Get title, description, labels, priority, and comments +- **Update issues**: Add labels and set priority +- **Create comments**: Post triage reasoning comments + +Start by listing available tools to discover the exact tool names and parameters. + +**Important:** When updating labels, preserve existing labels. Fetch the issue's current labels first, then include both old and new label IDs in the update. + +## Cache-Memory Format + +Store and read a JSON file at the **exact path** `cache-memory/triage-cache.json`. Always use this filename — never rename it or create alternative files. + +```json +{ + "triaged_issue_ids": ["BER-3150", "BER-3151"], + "last_run": "2025-01-15T10:00:00Z" +} +``` + +On each run: +1. Read `cache-memory/triage-cache.json` to get previously triaged issue identifiers +2. Skip any issues already in the list +3. After processing, write the updated list back to `cache-memory/triage-cache.json` (append newly triaged IDs) + +## Triage Rubric + +### Decision 1: Type Classification + +Classify each issue based on its title, description, and linked context: + +| Type | Signal words / patterns | Label to apply | +|------|------------------------|----------------| +| **Bug** | "broken", "doesn't work", "regression", "error", "crash", stack traces, Sentry links, "unexpected behaviour" | `🐛 Bug` (`e51776f7-038e-474b-86ec-66981c9abb4f`) | +| **Security** | "vulnerability", "exploit", "bypass", "SSRF", "XSS", "injection", "authentication bypass", "2FA", CVE references | `🔒 Security` (`28c5afc1-8063-4e62-af11-e42d94591957`) — also apply Bug if applicable | +| **Feature** | "add support for", "it would be nice", "can we", "new feature", Featurebase links | `✨ Feature` (`db8672e2-1053-4bc7-9aab-9d38c5b01560`) | +| **Improvement** | "improve", "enhance", "optimise", "refactor", "clean up", "polish" | `🎨 Improvement` (`b36579e6-62e1-4f55-987d-ee1e5c0cde1a`) | +| **Performance** | "slow", "latency", "timeout", "memory", "CPU", "performance", load time complaints | `⚡️ Performance` (`9066d0ea-6326-4b22-b6f5-82fe7ce2c1d1`) | +| **Maintenance** | "upgrade dependency", "tech debt", "remove deprecated", "migrate" | `🛠️ Maintenance` (`0ca27922-3646-4ab7-bf03-e67230c0c39e`) | +| **Documentation** | "docs", "README", "guide", "tutorial", missing documentation | `📝 Documentation` (`25f8988a-5925-44cd-b0df-c0229463925f`) | + +If an issue matches multiple types (e.g. a security bug), apply all relevant labels. + +### Decision 2: Priority Assignment + +Assign priority to all issue types. Set both the Linear priority field and the corresponding priority label. + +**For bugs and security issues**, use these criteria: + +#### P1 — Urgent (Linear priority: 1, Label: `📊 Priority → P1 - Urgent` `11de115f-3e40-46c6-bf42-2aa2b9195cbd`) +- Security vulnerability with a clear exploit path +- Data loss or corruption (MySQL, disk) — actual or imminent (exception: small lexical data issues can be P2) +- Multiple customers' businesses immediately affected (broken payment collection, broken emails, broken member login) + +#### P2 — High (Linear priority: 2, Label: `📊 Priority → P2 - High` `aeda47fa-9db9-4f4d-a446-3cccf92c8d12`) +- Triggering monitoring alerts that wake on-call engineers (if recurring, bump to P1) +- Security vulnerability without a clear exploit +- Regression that breaks currently working core functionality +- Crashes the server or browser +- Significantly disrupts customers' members/end-users (e.g. incorrect pricing or access) +- Bugs with members, subscriptions, or newsletters without immediate business impact + +#### P3 — Medium (Linear priority: 3, Label: `📊 Priority → P3 - Medium` `10ec8b7b-725f-453f-b5d2-ff160d3b3c1e`) +- Bugs with members, subscriptions, or newsletters affecting only a few customers +- Bugs in recently released features that significantly affect usability +- Issues with setup/upgrade flows +- Broken features (dashboards, line charts, analytics, etc.) +- Correctness issues (e.g. timezones) + +#### P4 — Low (Linear priority: 4, Label: `📊 Priority → P4 - Low` `411a21ea-c8c0-4cb1-9736-7417383620ff`) +- Not quite working as expected, but little overall impact +- Not related to payments, email, or security +- Significantly more complex to fix than the value of fixing +- Purely cosmetic +- Has a clear and straightforward workaround + +**For non-bug issues** (features, improvements, performance, maintenance, documentation), assign a **provisional priority** based on estimated impact and urgency. Clearly mark it as provisional in the triage comment. + +#### Bump Modifiers + +**Bump UP one level if:** +- It causes regular alerts for on-call engineers +- It affects lots of users or VIP customers +- It prevents users from carrying out a critical use case or workflow +- It prevents rolling back to a previous release + +**Bump DOWN one level if:** +- Reported by a single, non-VIP user +- Only impacts an edge case or obscure use case + +Note in your comment if a bump modifier was applied and why. + +### Decision 3: Product Area Tagging + +Apply the most relevant `Product Area →` label: + +| Label | Covers | +|-------|--------| +| `Product Area → Editor` | Post/page editor, Koenig, Lexical, content blocks | +| `Product Area → Dashboard` | Admin dashboard, stats, overview | +| `Product Area → Analytics` | Analytics, charts, reporting | +| `Product Area → Memberships` | Member management, segmentation, member data | +| `Product Area → Portal` | Member-facing portal, signup/login flows | +| `Product Area → Newsletters` | Email newsletters, sending, email design | +| `Product Area → Admin` | General admin UI, settings, navigation | +| `Product Area → Settings area` | Settings screens specifically | +| `Product Area → Billing App` | Billing, subscription management | +| `Product Area → Themes` | Theme system, Handlebars, theme marketplace | +| `Product Area → Publishing` | Post publishing, scheduling, distribution | +| `Product Area → Growth` | Growth features, recommendations | +| `Product Area → Comments` | Comment system | +| `Product Area → Imports / Exports` | Data import/export | +| `Product Area → Welcome emails / Automations` | Automated emails, welcome sequences | +| `Product Area → Social Web` | ActivityPub, federation | +| `Product Area → i18n` | Internationalisation, translations | +| `Product Area → Sodo Search` | Search functionality | +| `Product Area → Admin-X Offers` | Offers system in Admin-X | + +If the issue spans multiple areas, apply all relevant labels. If no product area is clearly identifiable, don't force a label — note this in the comment. + +**Important:** Use the Linear MCP tools to look up product area label IDs before applying them. + +### Decision 4: Triage Comment + +Post a comment on the issue with your reasoning. Use this format: + +``` +🤖 **Automated Triage** + +**Type:** Bug (Security) +**Priority:** P2 — High +**Product Area:** Memberships +**Bump modifiers applied:** UP — affects multiple customers + +**Reasoning:** +This appears to be a security vulnerability in the session handling that could allow +2FA bypass. While no clear exploit path has been reported, the potential for +authentication bypass affecting all staff accounts warrants P2. Bumped up from P3 +because it affects all customers with 2FA enabled. + +**Recommended action:** Code investigation recommended — this is a security bug +that needs code-level analysis. +``` + +For non-bug issues, mark priority as provisional: + +``` +🤖 **Automated Triage** + +**Type:** Improvement +**Priority:** P3 — Medium *(provisional)* +**Product Area:** Admin +**Bump modifiers applied:** None + +**Reasoning:** +This is a refactoring task to share logic between two related functions. No user-facing +impact, but reduces maintenance burden for the retention offers codebase. Provisional +P3 based on moderate codebase impact and alignment with active project work. + +**Recommended action:** Code investigation recommended — small refactoring task with +clear scope, no design input needed. +``` + +### Decision 5: Code Investigation Recommendation + +Flag an issue for code investigation in your comment if **all** of these are true: + +1. Classified as a bug, security issue, performance issue, or small improvement/maintenance task +2. Does not require design input (no UI mockups needed, no UX decisions) +3. Has enough description to investigate (not just a title with no context) + +Do **not** recommend investigation for: +- Feature requests (need product/design input) +- Issues with vague descriptions and no reproduction steps — instead note "Needs more info" in the comment +- Issues that are clearly large architectural changes + +## Guidelines + +- Process issues one at a time, applying all decisions before moving to the next +- Be concise but include enough reasoning that a human can quickly validate or override +- When in doubt about classification, pick the closest match and note your uncertainty +- If an issue already has triage labels or a triage comment from a previous run, skip it +- Never move issues out of the Triage state +- After processing all issues, update cache-memory with the full list of triaged identifiers diff --git a/.github/workflows/migration-review.yml b/.github/workflows/migration-review.yml new file mode 100644 index 0000000..c7a22eb --- /dev/null +++ b/.github/workflows/migration-review.yml @@ -0,0 +1,57 @@ +name: Migration Review +on: + pull_request_target: + types: [opened] + paths: + - 'ghost/core/core/server/data/schema/**' + - 'ghost/core/core/server/data/migrations/versions/**' +jobs: + createComment: + runs-on: ubuntu-latest + if: github.repository_owner == 'TryGhost' + name: Add migration review requirements + steps: + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ["migration"] + }) + + - uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + It looks like this PR contains a migration 👀 + Here's the checklist for reviewing migrations: + + ### General requirements + + - [ ] :warning: Tested performance on staging database servers, as performance on local machines is not comparable to a production environment + - [ ] Satisfies idempotency requirement (both `up()` and `down()`) + - [ ] Does not reference models + - [ ] Filename is in the correct format (and correctly ordered) + - [ ] Targets the next minor version + - [ ] All code paths have appropriate log messages + - [ ] Uses the correct utils + - [ ] Contains a minimal changeset + - [ ] Does not mix DDL/DML operations + - [ ] Tested in MySQL and SQLite + + ### Schema changes + + - [ ] Both schema change and related migration have been implemented + - [ ] For index changes: has been performance tested for large tables + - [ ] For new tables/columns: fields use the appropriate predefined field lengths + - [ ] For new tables/columns: field names follow the appropriate conventions + - [ ] Does not drop a non-alpha table outside of a major version + + ### Data changes + + - [ ] Mass updates/inserts are batched appropriately + - [ ] Does not loop over large tables/datasets + - [ ] Defends against missing or invalid data + - [ ] For settings updates: follows the appropriate guidelines diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..3dec4e8 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,137 @@ +name: PR Preview + +on: + pull_request_target: + types: [labeled, unlabeled, closed] + +jobs: + deploy: + name: Deploy Preview + # Runs when the "preview" label is added — requires collaborator write access + if: >- + github.event.action == 'labeled' + && github.event.label.name == 'preview' + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + steps: + - name: Wait for Docker build job + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_JOB_NAME: Build & Publish Artifacts + run: | + echo "Waiting for '${BUILD_JOB_NAME}' job to complete for $HEAD_SHA..." + TIMEOUT=1800 # 30 minutes + INTERVAL=30 + START=$(date +%s) + + while true; do + ELAPSED=$(( $(date +%s) - START )) + if [ "$ELAPSED" -ge "$TIMEOUT" ]; then + echo "::error::Timed out waiting for '${BUILD_JOB_NAME}' (${TIMEOUT}s)" + exit 1 + fi + + # Find the CI run for this SHA + RUN=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?head_sha=${HEAD_SHA}&per_page=1" \ + --jq '.workflow_runs[0] | {id, status}' 2>/dev/null || echo "") + + if [ -z "$RUN" ] || [ "$RUN" = "null" ]; then + echo " No CI run found yet, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + continue + fi + + RUN_ID=$(echo "$RUN" | jq -r '.id') + RUN_STATUS=$(echo "$RUN" | jq -r '.status') + + # Look up the build job specifically (paginate — CI has 30+ jobs) + BUILD_JOB=$(gh api --paginate "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + --jq ".jobs[] | select(.name == \"${BUILD_JOB_NAME}\") | {status, conclusion}") + + if [ -z "$BUILD_JOB" ]; then + if [ "$RUN_STATUS" = "completed" ]; then + echo "::error::CI run ${RUN_ID} completed but '${BUILD_JOB_NAME}' job was not found" + exit 1 + fi + echo " '${BUILD_JOB_NAME}' job not started yet (run ${RUN_STATUS}), waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + continue + fi + + JOB_STATUS=$(echo "$BUILD_JOB" | jq -r '.status') + JOB_CONCLUSION=$(echo "$BUILD_JOB" | jq -r '.conclusion // empty') + + if [ "$JOB_STATUS" = "completed" ]; then + if [ "$JOB_CONCLUSION" = "success" ]; then + echo "Docker build ready (CI run $RUN_ID)" + break + fi + echo "::error::'${BUILD_JOB_NAME}' did not succeed (conclusion: $JOB_CONCLUSION)" + exit 1 + fi + + echo " '${BUILD_JOB_NAME}' still ${JOB_STATUS}, waiting ${INTERVAL}s... (${ELAPSED}s elapsed)" + sleep "$INTERVAL" + done + + - name: Re-check PR eligibility + id: recheck + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \ + --jq '{state, labels: [.labels[].name]}') + + STATE=$(echo "$PR" | jq -r '.state') + HAS_LABEL=$(echo "$PR" | jq '.labels | any(. == "preview")') + + if [ "$STATE" != "open" ]; then + echo "::warning::PR is no longer open ($STATE), skipping dispatch" + echo "skip=true" >> "$GITHUB_OUTPUT" + elif [ "$HAS_LABEL" != "true" ]; then + echo "::warning::preview label was removed, skipping dispatch" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "PR still eligible for preview deploy" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Dispatch deploy to Ghost-Moya + if: steps.recheck.outputs.skip != 'true' + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 + with: + token: ${{ secrets.CANARY_DOCKER_BUILD }} + repository: TryGhost/Ghost-Moya + event-type: preview-deploy + client-payload: >- + { + "pr_number": "${{ github.event.pull_request.number }}", + "action": "deploy", + "seed": "true" + } + + destroy: + name: Destroy Preview + # Runs when "preview" label is removed, or the PR is closed/merged while labeled + if: >- + (github.event.action == 'unlabeled' && github.event.label.name == 'preview') + || (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Dispatch destroy to Ghost-Moya + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4 + with: + token: ${{ secrets.CANARY_DOCKER_BUILD }} + repository: TryGhost/Ghost-Moya + event-type: preview-destroy + client-payload: >- + { + "pr_number": "${{ github.event.pull_request.number }}", + "action": "destroy" + } diff --git a/.github/workflows/publish-tb-cli.yml b/.github/workflows/publish-tb-cli.yml new file mode 100644 index 0000000..e7de9a9 --- /dev/null +++ b/.github/workflows/publish-tb-cli.yml @@ -0,0 +1,46 @@ +name: Publish tb-cli Image + +on: + workflow_dispatch: # Manual trigger from GitHub UI or CLI + push: + branches: [main] + paths: + - 'docker/tb-cli/**' + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Build and push tb-cli to GHCR + runs-on: ubuntu-latest + if: github.repository == 'TryGhost/Ghost' && github.ref == 'refs/heads/main' + concurrency: + group: publish-tb-cli + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + + - name: Login to GHCR + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 + with: + context: . + file: docker/tb-cli/Dockerfile + push: true + tags: | + ghcr.io/tryghost/tb-cli:latest + ghcr.io/tryghost/tb-cli:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0ad9370 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release +run-name: "Release — ${{ inputs.bump-type || 'auto' }} from ${{ inputs.branch || 'main' }}${{ inputs.dry-run && ' (dry run)' || '' }}" + +on: + schedule: + - cron: '0 15 * * 5' # Friday 3pm UTC + workflow_dispatch: + inputs: + branch: + description: 'Git branch to release from' + type: string + default: 'main' + required: false + bump-type: + description: 'Version bump type (auto, patch, minor)' + type: string + required: false + default: 'auto' + skip-checks: + description: 'Skip CI status check verification' + type: boolean + default: false + dry-run: + description: 'Dry run (version bump without push)' + type: boolean + default: false + +env: + FORCE_COLOR: 1 + NODE_VERSION: 22.18.0 +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + name: Prepare & Push Release + steps: + - uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_KEY }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Deploy key (via ssh-agent) is used for git push — it bypasses + # branch protection and triggers downstream workflows (unlike GITHUB_TOKEN) + ref: ${{ inputs.branch || 'main' }} + fetch-depth: 0 + ssh-key: ${{ secrets.DEPLOY_KEY }} + + # Fetch submodules separately via HTTPS — the deploy key is scoped to + # Ghost only and can't authenticate against Casper/Source over SSH + - run: git submodule update --init + + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + env: + FORCE_COLOR: 0 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Set up Git + run: | + git config user.name "Ghost CI" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Set up schedule defaults + if: github.event_name == 'schedule' + run: | + echo "RELEASE_BRANCH=main" >> "$GITHUB_ENV" + echo "RELEASE_BUMP_TYPE=auto" >> "$GITHUB_ENV" + echo "RELEASE_DRY_RUN=" >> "$GITHUB_ENV" + echo "RELEASE_SKIP_CHECKS=" >> "$GITHUB_ENV" + + - name: Set up workflow_dispatch inputs + if: github.event_name == 'workflow_dispatch' + run: | + echo "RELEASE_BRANCH=${INPUT_BRANCH}" >> "$GITHUB_ENV" + echo "RELEASE_BUMP_TYPE=${INPUT_BUMP_TYPE}" >> "$GITHUB_ENV" + echo "RELEASE_DRY_RUN=${INPUT_DRY_RUN}" >> "$GITHUB_ENV" + echo "RELEASE_SKIP_CHECKS=${INPUT_SKIP_CHECKS}" >> "$GITHUB_ENV" + env: + INPUT_BRANCH: ${{ inputs.branch }} + INPUT_BUMP_TYPE: ${{ inputs.bump-type }} + INPUT_DRY_RUN: ${{ inputs.dry-run }} + INPUT_SKIP_CHECKS: ${{ inputs.skip-checks }} + + - name: Run release script + run: | + ARGS="--branch=${{ env.RELEASE_BRANCH }} --bump-type=${{ env.RELEASE_BUMP_TYPE }}" + if [ "${{ env.RELEASE_DRY_RUN }}" = "true" ]; then + ARGS="$ARGS --dry-run" + fi + if [ "${{ env.RELEASE_SKIP_CHECKS }}" = "true" ]; then + ARGS="$ARGS --skip-checks" + fi + node scripts/release.js $ARGS + env: + GITHUB_TOKEN: ${{ secrets.CANARY_DOCKER_BUILD }} # PAT for GitHub API (check polling) + + - name: Notify on failure + if: failure() + uses: tryghost/actions/actions/slack-build@20b5ae5f266e86f7b5f0815d92731d6388b8ce46 # main + with: + status: ${{ job.status }} + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/stale-i18n.yml b/.github/workflows/stale-i18n.yml new file mode 100644 index 0000000..f1c7351 --- /dev/null +++ b/.github/workflows/stale-i18n.yml @@ -0,0 +1,26 @@ +name: 'Close stale i18n PRs' +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' +jobs: + stale: + if: github.repository_owner == 'TryGhost' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10 + with: + stale-pr-message: | + Thanks for contributing to Ghost's i18n :) + + This PR has been automatically marked as stale because there has not been any activity here in 3 weeks. + I18n PRs tend to get out of date quickly, so we're closing them to keep the PR list clean. + + If you're still interested in working on this PR, please let us know. Otherwise this PR will be closed shortly, but can always be reopened later. Thank you for understanding 🙂 + only-labels: 'affects:i18n' + days-before-pr-stale: 21 + days-before-pr-close: 7 + exempt-pr-labels: 'feature,pinned,needs:triage' + stale-pr-label: 'stale' + close-pr-message: | + This PR has been automatically closed due to inactivity. If you'd like to continue working on it, feel free to open a new PR. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..6252b95 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,29 @@ +name: 'Close stale issues and PRs' +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' +jobs: + stale: + if: github.repository_owner == 'TryGhost' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10 + with: + stale-issue-message: | + Our bot has automatically marked this issue as stale because there has not been any activity here in some time. + + The issue will be closed soon if there are no further updates, however we ask that you do not post comments to keep the issue open if you are not actively working on a PR. + + We keep the issue list minimal so we can keep focus on the most pressing issues. Closed issues can always be reopened if a new contributor is found. Thank you for understanding 🙂 + stale-pr-message: | + Our bot has automatically marked this PR as stale because there has not been any activity here in some time. + + If we’ve missed reviewing your PR & you’re still interested in working on it, please let us know. Otherwise this PR will be closed shortly, but can always be reopened later. Thank you for understanding 🙂 + exempt-issue-labels: 'feature,pinned,needs:triage' + exempt-pr-labels: 'feature,pinned,needs:triage' + days-before-stale: 113 + days-before-pr-stale: 358 + stale-issue-label: 'stale' + stale-pr-label: 'stale' + close-issue-reason: 'not_planned' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a2097b --- /dev/null +++ b/.gitignore @@ -0,0 +1,211 @@ +# Node template + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +coverage* + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Nx +.nxcache +.nx/cache +.nx/workspace-data + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# pnpm store +.pnpm-store + +# dotenv environment variables file +.env + +# IDE +.idea/ +*.iml +*.sublime-* +.vscode/* +!.vscode/launch.json +!.vscode/settings.json + +# OSX +.DS_Store + +!test/utils/fixtures/**/*.csv + +# Ghost DB file +*.db +*.db-journal + +/ghost/core/test-results/ +/ghost/core/core/server/data/export/exported* +/ghost/core/content/tmp/* +/ghost/core/content/data/* +/ghost/core/content/logs/* +/ghost/core/content/settings/* +/ghost/core/content/apps/**/* +/ghost/core/content/themes/**/* +/ghost/core/content/images/**/* +/ghost/core/content/media/**/* +/ghost/core/content/files/**/* +/ghost/core/content/public/* +/ghost/core/content/adapters/storage/**/* +/ghost/core/content/adapters/scheduling/**/* +/ghost/core/content/themes/casper +/ghost/core/content/themes/source +!/ghost/core/README.md +!/ghost/core/content/**/README.md + +# Changelog, which is autogenerated, not committed +/ghost/core/CHANGELOG.md + +# Assets bundled into the release but we don't want to commit +/ghost/core/LICENSE +/ghost/core/PRIVACY.md +/ghost/core/README.md +/ghost/core/pnpm-lock.yaml + +# Test generated files +test/functional/*.png + +# ignore all custom json files for config +/ghost/core/config.*.json +/ghost/core/config.*.jsonc + +# Built asset files +/ghost/core/core/built +/ghost/core/core/frontend/public/ghost.min.css +/ghost/core/core/frontend/public/comment-counts.min.js +/ghost/core/core/frontend/public/member-attribution.min.js +/ghost/core/core/frontend/public/ghost-stats.min.js +/ghost/core/core/frontend/public/private.min.js +# Caddyfile - for local development with ssl + caddy +Caddyfile +!docker/caddy/Caddyfile +!docker/dev-gateway/Caddyfile + +# Playwright state with cookies it keeps across tests +/ghost/core/playwright-state.json +/ghost/core/playwright-report +/playwright-report +/test-results + +# Admin +/ghost/admin/dist + +# Comments-UI +/apps/comments-ui/umd +/apps/comments-ui/playwright-report +/ghost/comments-ui/playwright/.cache/ +/apps/comments-ui/test-results/ + +# Portal +!/apps/portal/.env +/apps/portal/umd + +# Sodo-Search +/apps/sodo-search/public/main.css +/apps/sodo-search/umd + +# Signup Form and local environments +/apps/signup-form/umd +/apps/signup-form/.env*.local +/apps/signup-form/test-results/ +/apps/signup-form/playwright-report/ +/apps/signup-form/playwright/.cache/ + +# Announcement-Bar +/apps/announcement-bar/umd + +# Build files +/apps/*/build +/ghost/*/build +# Typescript build artifacts +tsconfig.tsbuildinfo + +# Admin X +/apps/admin-x-settings/dist +/apps/admin-x-settings/dist-ssr +/apps/admin-x-settings/test-results/ +/apps/admin-x-settings/playwright-report/ +/apps/admin-x-settings/playwright/.cache/ + +# Tinybird +.tinyb +.venv +.diff_tmp +temp*.sql + +# Docker pnpm Cache +.pnpmhash + +# yalc — for linking local packages in a docker compatible way +yalc.lock +.yalc + +# A folder for AI generated files +# useful for keeping local plans etc +/ai + +# direnv environment loader files +.envrc + +# Private Claude Code instructions +*.local.md +.claude/settings.local.json +.mcp.local.json + +# e2e test suite +/e2e/test-results +/e2e/playwright-report +/e2e/build +/e2e/playwright +/e2e/data +.env.tinybird +.cursor/rules/nx-rules.mdc +.github/instructions/nx.instructions.md diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1df7c0e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "ghost/core/content/themes/casper"] + path = ghost/core/content/themes/casper + url = ../../TryGhost/Casper.git + ignore = all +[submodule "ghost/core/content/themes/source"] + path = ghost/core/content/themes/source + url = ../../TryGhost/Source.git + ignore = all diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..43aefb9 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,69 @@ +const path = require('path'); + +const SCOPED_WORKSPACES = [ + 'e2e', + 'apps/admin', + 'apps/posts', + 'apps/shade' +]; + +function normalize(file) { + return file.split(path.sep).join('/'); +} + +function isInWorkspace(file, workspace) { + const normalizedFile = normalize(path.relative(process.cwd(), file)); + const normalizedWorkspace = normalize(workspace); + + return normalizedFile === normalizedWorkspace || normalizedFile.startsWith(`${normalizedWorkspace}/`); +} + +function shellQuote(value) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function buildScopedEslintCommand(workspace, files) { + if (files.length === 0) { + return null; + } + + const relativeFiles = files + .map(file => normalize(path.relative(workspace, file))) + .map(shellQuote) + .join(' '); + + return `pnpm --dir ${shellQuote(workspace)} exec eslint --cache ${relativeFiles}`; +} + +function buildRootEslintCommand(files) { + if (files.length === 0) { + return null; + } + + const quotedFiles = files.map(file => shellQuote(normalize(file))).join(' '); + return `eslint --cache ${quotedFiles}`; +} + +module.exports = { + '*.{js,ts,tsx,jsx,cjs}': (files) => { + const workspaceGroups = new Map(SCOPED_WORKSPACES.map(workspace => [workspace, []])); + const rootFiles = []; + + for (const file of files) { + const workspace = SCOPED_WORKSPACES.find(candidate => isInWorkspace(file, candidate)); + + if (workspace) { + workspaceGroups.get(workspace).push(file); + } else { + rootFiles.push(file); + } + } + + return [ + ...SCOPED_WORKSPACES + .map(workspace => buildScopedEslintCommand(workspace, workspaceGroups.get(workspace))) + .filter(Boolean), + buildRootEslintCommand(rootFiles) + ].filter(Boolean); + } +}; diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..3b01b19 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +shamefully-hoist=false +engine-strict=false diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..398e21d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,54 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "args": [ + "--require", + "./test/utils/overrides.js", + "-u", + "bdd", + "--timeout", + "999999", + "--colors", + "./test/e2e-api/**/*.test.js" + ], + "cwd": "${workspaceFolder}/ghost/core", + "internalConsoleOptions": "openOnSessionStart", + "name": "E2E API Tests", + "program": "./node_modules/.bin/_mocha", + "request": "launch", + "skipFiles": [ + "/**" + ], + "env": { + "NODE_ENV": "testing-mysql" + }, + "type": "node" + }, + { + "args": [ + "-u", + "bdd", + "--timeout", + "999999", + "--colors", + "./test/**/*.test.js" + ], + "cwd": "${workspaceFolder}/ghost/email-service/", + "internalConsoleOptions": "openOnSessionStart", + "name": "Email Service Unit Tests", + "program": "./node_modules/.bin/_mocha", + "request": "launch", + "skipFiles": [ + "/**" + ], + "env": { + "NODE_ENV": "testing-mysql" + }, + "type": "node" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7659b90 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,31 @@ +{ + "editor.quickSuggestions": { + "strings": true + }, + "eslint.workingDirectories": [ + { + "pattern": "./apps/*/" + }, + { + "pattern": "./ghost/*/" + } + ], + "search.exclude": { + "**/.git": true, + "**/build/*": true, + "**/coverage/**": true, + "**/dist/**": true, + "**/ghost.map": true, + "**/node_modules": true, + "ghost/core/core/built/**": true, + "**/config.*.json": false, + "**/config.*.jsonc": false + }, + "tailwindCSS.experimental.classRegex": [ + ["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ], + "git.detectSubmodules": false, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d39c42a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,283 @@ +# AGENTS.md + +This file provides guidance to AI Agents when working with code in this repository. + +## Package Manager + +**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm. + +## Monorepo Structure + +Ghost is a pnpm + Nx monorepo with three workspace groups: + +### ghost/* - Core Ghost packages +- **ghost/core** - Main Ghost application (Node.js/Express backend) + - Core server: `ghost/core/core/server/` + - Frontend rendering: `ghost/core/core/frontend/` +- **ghost/admin** - Ember.js admin client (legacy, being migrated to React) +- **ghost/i18n** - Centralized internationalization for all apps + +### apps/* - React-based UI applications +Two categories of apps: + +**Admin Apps** (embedded in Ghost Admin): +- `admin-x-settings`, `admin-x-activitypub` - Settings and integrations +- `posts`, `stats` - Post analytics and site-wide analytics +- Built with Vite + React + `@tanstack/react-query` + +**Public Apps** (served to site visitors): +- `portal`, `comments-ui`, `signup-form`, `sodo-search`, `announcement-bar` +- Built as UMD bundles, loaded via CDN in site themes + +**Foundation Libraries**: +- `admin-x-framework` - Shared API hooks, routing, utilities +- `admin-x-design-system` - Legacy design system (being phased out) +- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod) + +### e2e/ - End-to-end tests +- Playwright-based E2E tests with Docker container isolation +- See `e2e/CLAUDE.md` for detailed testing guidance + +## Common Commands + +### Development +```bash +corepack enable pnpm # Enable corepack to use the correct pnpm version +pnpm run setup # First-time setup (installs deps + submodules) +pnpm dev # Start development (Docker backend + host frontend dev servers) +``` + +### Building +```bash +pnpm build # Build all packages (Nx handles dependencies) +pnpm build:clean # Clean build artifacts and rebuild +``` + +### Testing +```bash +# Unit tests (from root) +pnpm test:unit # Run all unit tests in all packages + +# Ghost core tests (from ghost/core/) +cd ghost/core +pnpm test:unit # Unit tests only +pnpm test:integration # Integration tests +pnpm test:e2e # E2E API tests (not browser) +pnpm test:all # All test types + +# E2E browser tests (from root) +pnpm test:e2e # Run e2e/ Playwright tests + +# Running a single test +cd ghost/core +pnpm test:single test/unit/path/to/test.test.js +``` + +### Linting +```bash +pnpm lint # Lint all packages +cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests) +cd ghost/admin && pnpm lint # Lint Ember admin +``` + +### Database +```bash +pnpm knex-migrator migrate # Run database migrations +pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running) +pnpm reset:data:empty # Reset database with no data (requires pnpm dev running) +``` + +### Docker +```bash +pnpm docker:build # Build Docker images +pnpm docker:clean # Stop containers, remove volumes and local images +pnpm docker:down # Stop containers +``` + +### How `pnpm dev` works + +The `pnpm dev` command uses a **hybrid Docker + host development** setup: + +**What runs in Docker:** +- Ghost Core backend (with hot-reload via mounted source) +- MySQL, Redis, Mailpit +- Caddy gateway/reverse proxy + +**What runs on host:** +- Frontend dev servers (Admin, Portal, Comments UI, etc.) in watch mode with HMR +- Foundation libraries (shade, admin-x-framework, etc.) + +**Setup:** +```bash +# Start everything (Docker + frontend dev servers) +pnpm dev + +# With optional services (uses Docker Compose file composition) +pnpm dev:analytics # Include Tinybird analytics +pnpm dev:storage # Include MinIO S3-compatible object storage +pnpm dev:all # Include all optional services +``` + +**Accessing Services:** +- Ghost: `http://localhost:2368` (database: `ghost_dev`) +- Mailpit UI: `http://localhost:8025` (email testing) +- MySQL: `localhost:3306` +- Redis: `localhost:6379` +- Tinybird: `http://localhost:7181` (when analytics enabled) +- MinIO Console: `http://localhost:9001` (when storage enabled) +- MinIO S3 API: `http://localhost:9000` (when storage enabled) + +## Architecture Patterns + +### Admin Apps Integration (Micro-Frontend) + +**Build Process:** +1. Admin-x React apps build to `apps/*/dist` using Vite +2. `ghost/admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*` +3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js` + +**Runtime Loading:** +- Ember admin uses `AdminXComponent` to dynamically import React apps +- React components wrapped in Suspense with error boundaries +- Apps receive config via `additionalProps()` method + +### Public Apps Integration + +- Built as UMD bundles to `apps/*/umd/*.min.js` +- Loaded via ` + + diff --git a/apps/activitypub/package.json b/apps/activitypub/package.json new file mode 100644 index 0000000..02fcabc --- /dev/null +++ b/apps/activitypub/package.json @@ -0,0 +1,95 @@ +{ + "name": "@tryghost/activitypub", + "version": "3.1.13", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TryGhost/Ghost/tree/main/apps/activitypub" + }, + "author": "Ghost Foundation", + "files": [ + "LICENSE", + "README.md", + "dist/" + ], + "main": "./dist/activitypub.umd.cjs", + "module": "./dist/activitypub.js", + "exports": { + ".": { + "import": "./dist/activitypub.js", + "require": "./dist/activitypub.umd.cjs" + }, + "./api": "./src/index.tsx" + }, + "private": false, + "scripts": { + "dev": "vite build --watch", + "dev:start": "vite", + "build": "tsc && vite build", + "lint": "pnpm run lint:code && pnpm run lint:test", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test", + "test": "pnpm test:unit", + "test:unit": "tsc --noEmit && vitest run", + "test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", + "test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance --headed", + "test:acceptance:full": "ALL_BROWSERS=1 pnpm test:acceptance", + "preview": "vite preview" + }, + "devDependencies": { + "@playwright/test": "1.59.1", + "@testing-library/react": "14.3.1", + "@types/dompurify": "3.2.0", + "@types/jest": "29.5.14", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "jest": "29.7.0", + "tailwindcss": "^4.2.2", + "ts-jest": "29.4.9", + "vite": "5.4.21", + "vitest": "1.6.1" + }, + "nx": { + "targets": { + "build": { + "dependsOn": [ + "^build" + ] + }, + "dev": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build", + "test:unit" + ] + }, + "test:acceptance": { + "dependsOn": [ + "^build", + "test:acceptance" + ] + } + } + }, + "dependencies": { + "@hookform/resolvers": "5.2.2", + "@radix-ui/react-form": "0.1.8", + "@tanstack/react-query": "4.36.1", + "@tryghost/admin-x-framework": "workspace:*", + "@tryghost/shade": "workspace:*", + "clsx": "2.1.1", + "dompurify": "3.3.1", + "html2canvas-objectfit-fix": "1.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-hook-form": "7.72.1", + "react-router": "7.14.0", + "sonner": "2.0.7", + "use-debounce": "10.1.1", + "zod": "4.1.12" + } +} diff --git a/apps/activitypub/playwright.config.mjs b/apps/activitypub/playwright.config.mjs new file mode 100644 index 0000000..8fa5955 --- /dev/null +++ b/apps/activitypub/playwright.config.mjs @@ -0,0 +1,3 @@ +import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright'; + +export default adminXPlaywrightConfig(); diff --git a/apps/activitypub/src/app.tsx b/apps/activitypub/src/app.tsx new file mode 100644 index 0000000..a8b6f5c --- /dev/null +++ b/apps/activitypub/src/app.tsx @@ -0,0 +1,29 @@ +import {FeatureFlagsProvider} from './lib/feature-flags'; +import {FrameworkProvider, Outlet, RouterProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; +import {ShadeApp} from '@tryghost/shade/app'; +import {routes} from '@src/routes'; + +interface AppProps { + framework: TopLevelFrameworkProps; + activityPubEnabled?: boolean; +} + +const App: React.FC = ({framework, activityPubEnabled}) => { + if (activityPubEnabled === false) { + return null; + } + + return ( + + + + + + + + + + ); +}; + +export default App; diff --git a/apps/activitypub/src/index.tsx b/apps/activitypub/src/index.tsx new file mode 100644 index 0000000..cbd5aa0 --- /dev/null +++ b/apps/activitypub/src/index.tsx @@ -0,0 +1,7 @@ +import './styles/index.css'; + +export {default as AdminXApp} from './app'; + +export {routes} from './routes'; +export {FeatureFlagsProvider} from './lib/feature-flags'; +export {useNotificationsCountForUser} from './hooks/use-activity-pub-queries'; diff --git a/apps/activitypub/src/routes.tsx b/apps/activitypub/src/routes.tsx new file mode 100644 index 0000000..3ee76ce --- /dev/null +++ b/apps/activitypub/src/routes.tsx @@ -0,0 +1,142 @@ +import AppError from '@components/layout/error'; + +import {Navigate, Outlet, RouteObject, lazyComponent} from '@tryghost/admin-x-framework'; + +const basePath = import.meta.env.VITE_TEST ? '' : 'activitypub'; + +export type CustomRouteObject = RouteObject & { + pageTitle?: string; + children?: CustomRouteObject[]; + showBackButton?: boolean; +}; + +export const routes: CustomRouteObject[] = [ + { + // Root route that defines the app's base path + path: basePath, + element: , + errorElement: , // This will catch all errors in child routes + handle: 'activitypub-basepath', + children: [ + { + index: true, + element: + }, + { + path: 'inbox', + element: + }, + { + path: 'feed', + element: + }, + { + path: 'reader', + lazy: lazyComponent(() => import('./views/inbox')), + pageTitle: 'Reader' + }, + { + path: 'reader/:postId', + lazy: lazyComponent(() => import('./views/inbox')), + pageTitle: 'Reader' + }, + { + path: 'notes', + lazy: lazyComponent(() => import('./views/feed/feed')), + pageTitle: 'Notes' + }, + { + path: 'notes/:postId', + lazy: lazyComponent(() => import('./views/feed/note')), + pageTitle: 'Note' + }, + { + path: 'notifications', + lazy: lazyComponent(() => import('./views/notifications')), + pageTitle: 'Notifications' + }, + { + path: 'explore', + lazy: lazyComponent(() => import('./views/explore')), + pageTitle: 'Explore' + }, + { + path: 'explore/:topic', + lazy: lazyComponent(() => import('./views/explore')), + pageTitle: 'Explore' + }, + { + path: 'profile', + lazy: lazyComponent(() => import('./views/profile')), + pageTitle: 'Profile' + }, + { + path: 'profile/likes', + lazy: lazyComponent(() => import('./views/profile')), + pageTitle: 'Profile' + }, + { + path: 'profile/following', + lazy: lazyComponent(() => import('./views/profile')), + pageTitle: 'Profile' + }, + { + path: 'profile/followers', + lazy: lazyComponent(() => import('./views/profile')), + pageTitle: 'Profile' + }, + { + path: 'profile/:handle/:tab?', + lazy: lazyComponent(() => import('./views/profile')), + pageTitle: 'Profile' + }, + { + path: 'preferences', + lazy: lazyComponent(() => import('./views/preferences')), + pageTitle: 'Preferences' + }, + { + path: 'preferences/moderation', + lazy: lazyComponent(() => import('./views/preferences/components/moderation')), + pageTitle: 'Moderation', + showBackButton: true + }, + { + path: 'preferences/bluesky-sharing', + lazy: lazyComponent(() => import('./views/preferences/components/bluesky-sharing')), + showBackButton: true + }, + { + path: 'welcome', + lazy: lazyComponent(() => import('./components/layout/onboarding')), + pageTitle: 'Welcome', + children: [ + { + path: '', + element: + }, + { + path: '1', + lazy: lazyComponent(() => import('./components/layout/onboarding/step-1')) + }, + { + path: '2', + lazy: lazyComponent(() => import('./components/layout/onboarding/step-2')) + }, + { + path: '3', + lazy: lazyComponent(() => import('./components/layout/onboarding/step-3')) + }, + { + path: '*', + element: + } + ] + }, + { + path: '*', + lazy: lazyComponent(() => import('./components/layout/error')) + } + ] + } +]; diff --git a/apps/activitypub/src/standalone.tsx b/apps/activitypub/src/standalone.tsx new file mode 100644 index 0000000..c4419c5 --- /dev/null +++ b/apps/activitypub/src/standalone.tsx @@ -0,0 +1,5 @@ +import './styles/index.css'; +import App from './app.tsx'; +import renderStandaloneApp from '@tryghost/admin-x-framework/test/render'; + +renderStandaloneApp(App, {}); diff --git a/apps/activitypub/test/.eslintrc.cjs b/apps/activitypub/test/.eslintrc.cjs new file mode 100644 index 0000000..7e1fcbf --- /dev/null +++ b/apps/activitypub/test/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/ts-test' + ], + rules: { + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/apps/activitypub/tsconfig.declaration.json b/apps/activitypub/tsconfig.declaration.json new file mode 100644 index 0000000..d26eefa --- /dev/null +++ b/apps/activitypub/tsconfig.declaration.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} + \ No newline at end of file diff --git a/apps/activitypub/tsconfig.json b/apps/activitypub/tsconfig.json new file mode 100644 index 0000000..bce3e49 --- /dev/null +++ b/apps/activitypub/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client", "jest"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": "./src", + "paths": { + "@src/*": ["*"], + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@hooks/*": ["hooks/*"], + "@utils/*": ["utils/*"], + "@views/*": ["views/*"] + } + }, + "include": ["src", "test"] +} diff --git a/apps/activitypub/vite.config.mjs b/apps/activitypub/vite.config.mjs new file mode 100644 index 0000000..9501bd7 --- /dev/null +++ b/apps/activitypub/vite.config.mjs @@ -0,0 +1,72 @@ +import adminXViteConfig from '@tryghost/admin-x-framework/vite'; +import pkg from './package.json'; +import {resolve} from 'path'; +import fs from 'fs'; + +const GHOST_CARDS_PATH = resolve(__dirname, '../../ghost/core/core/frontend/src/cards'); + +const validateCardsDirectoryPlugin = (cardsPath) => { + return { + name: 'validate-cards-directory', + buildStart() { + const jsPath = resolve(cardsPath, 'js'); + const cssPath = resolve(cardsPath, 'css'); + + if (!fs.existsSync(cardsPath)) { + throw new Error(`Ghost cards directory not found at: ${cardsPath}`); + } + + if (!fs.existsSync(jsPath)) { + throw new Error(`Ghost cards JS directory not found at: ${jsPath}`); + } + + if (!fs.existsSync(cssPath)) { + throw new Error(`Ghost cards CSS directory not found at: ${cssPath}`); + } + + const jsFiles = fs.readdirSync(jsPath).filter(f => f.endsWith('.js')); + const cssFiles = fs.readdirSync(cssPath).filter(f => f.endsWith('.css')); + + if (jsFiles.length === 0) { + throw new Error(`No JavaScript files found in Ghost cards directory: ${jsPath}`); + } + + if (cssFiles.length === 0) { + throw new Error(`No CSS files found in Ghost cards directory: ${cssPath}`); + } + + console.log(`✓ Found ${jsFiles.length} JS and ${cssFiles.length} CSS card files at: ${cardsPath}`); + } + }; +}; + +export default (function viteConfig() { + const config = adminXViteConfig({ + packageName: pkg.name, + entry: resolve(__dirname, 'src/index.tsx'), + overrides: { + test: { + include: [ + './test/unit/**/*', + './src/**/*.test.ts' + ] + }, + resolve: { + alias: { + '@src': resolve(__dirname, './src'), + '@assets': resolve(__dirname, './src/assets'), + '@components': resolve(__dirname, './src/components'), + '@hooks': resolve(__dirname, './src/hooks'), + '@utils': resolve(__dirname, './src/utils'), + '@views': resolve(__dirname, './src/views'), + '@ghost-cards': GHOST_CARDS_PATH + } + }, + plugins: [ + validateCardsDirectoryPlugin(GHOST_CARDS_PATH) + ] + } + }); + + return config; +}); diff --git a/apps/admin-x-design-system/.eslintrc.cjs b/apps/admin-x-design-system/.eslintrc.cjs new file mode 100644 index 0000000..531e0ae --- /dev/null +++ b/apps/admin-x-design-system/.eslintrc.cjs @@ -0,0 +1,49 @@ +const tailwindCssConfig = `${__dirname}/../admin/src/index.css`; + +module.exports = { + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + }, + tailwindcss: { + config: tailwindCssConfig + } + }, + rules: { + // suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/enforces-negative-arbitrary-values': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/migration-from-tailwind-2': 'warn', + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': 'error' + } +}; diff --git a/apps/admin-x-design-system/.gitignore b/apps/admin-x-design-system/.gitignore new file mode 100644 index 0000000..62ac7b7 --- /dev/null +++ b/apps/admin-x-design-system/.gitignore @@ -0,0 +1,2 @@ +es +types diff --git a/apps/admin-x-design-system/.storybook/Inter.ttf b/apps/admin-x-design-system/.storybook/Inter.ttf new file mode 100644 index 0000000..1cb674b Binary files /dev/null and b/apps/admin-x-design-system/.storybook/Inter.ttf differ diff --git a/apps/admin-x-design-system/.storybook/adminx-theme.tsx b/apps/admin-x-design-system/.storybook/adminx-theme.tsx new file mode 100644 index 0000000..8a64dd1 --- /dev/null +++ b/apps/admin-x-design-system/.storybook/adminx-theme.tsx @@ -0,0 +1,38 @@ +import {create} from '@storybook/theming/create'; + +export default create({ + base: 'light', + // Typography + fontBase: '"Inter", sans-serif', + fontCode: 'monospace', + + brandTitle: 'AdminX Design System', + brandUrl: 'https://ghost.org', + brandImage: 'https://github.com/peterzimon/playground/assets/353959/c4358b4e-232f-4dba-8abb-adb3142ccd89', + brandTarget: '_self', + + // + colorPrimary: '#30CF43', + colorSecondary: '#15171A', + + // UI + appBg: '#ffffff', + appContentBg: '#ffffff', + appBorderColor: '#EBEEF0', + appBorderRadius: 0, + + // Text colors + textColor: '#15171A', + textInverseColor: '#ffffff', + + // Toolbar default and active colors + barTextColor: '#9E9E9E', + barSelectedColor: '#15171A', + barBg: '#ffffff', + + // Form colors + inputBg: '#ffffff', + inputBorder: '#15171A', + inputTextColor: '#15171A', + inputBorderRadius: 2, +}); \ No newline at end of file diff --git a/apps/admin-x-design-system/.storybook/main.tsx b/apps/admin-x-design-system/.storybook/main.tsx new file mode 100644 index 0000000..53b7664 --- /dev/null +++ b/apps/admin-x-design-system/.storybook/main.tsx @@ -0,0 +1,27 @@ +import type { StorybookConfig } from "@storybook/react-vite"; + +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + { + name: '@storybook/addon-styling', + }, + ], + framework: { + name: "@storybook/react-vite", + options: {}, + }, + docs: { + autodocs: "tag", + }, + async viteFinal(config, options) { + config.resolve!.alias = { + crypto: require.resolve('rollup-plugin-node-builtins') + } + return config; + } +}; +export default config; diff --git a/apps/admin-x-design-system/.storybook/manager.tsx b/apps/admin-x-design-system/.storybook/manager.tsx new file mode 100644 index 0000000..8e66cc9 --- /dev/null +++ b/apps/admin-x-design-system/.storybook/manager.tsx @@ -0,0 +1,6 @@ +import {addons} from '@storybook/manager-api'; +import adminxTheme from './adminx-theme'; + +addons.setConfig({ + theme: adminxTheme +}); \ No newline at end of file diff --git a/apps/admin-x-design-system/.storybook/preview.tsx b/apps/admin-x-design-system/.storybook/preview.tsx new file mode 100644 index 0000000..d0282a9 --- /dev/null +++ b/apps/admin-x-design-system/.storybook/preview.tsx @@ -0,0 +1,107 @@ +import React from 'react'; + +import '../styles.css'; +import './storybook.css'; + +import type { Preview } from "@storybook/react"; +import DesignSystemProvider from '../src/providers/design-system-provider'; +import adminxTheme from './adminx-theme'; + +// import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; + +const customViewports = { + sm: { + name: 'sm', + styles: { + width: '480px', + height: '801px', + }, + }, + md: { + name: 'md', + styles: { + width: '640px', + height: '801px', + }, + }, + lg: { + name: 'lg', + styles: { + width: '1024px', + height: '801px', + }, + }, + xl: { + name: 'xl', + styles: { + width: '1320px', + height: '801px', + }, + }, + tablet: { + name: 'tablet', + styles: { + width: '860px', + height: '801px', + }, + }, +}; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: { + method: 'alphabetical', + order: ['Welcome', 'Foundations', ['Style Guide', 'Colors', 'Icons', 'ErrorHandling'], 'Global', ['Form', 'Chrome', 'Modal', 'Layout', ['View Container', 'Page Header', 'Page'], 'List', 'Table', '*'], 'Settings', ['Setting Section', 'Setting Group', '*'], 'Experimental'], + }, + }, + docs: { + theme: adminxTheme, + }, + viewport: { + viewports: { + ...customViewports, + }, + }, + }, + decorators: [ + (Story, context) => { + let {scheme} = context.globals; + + return ( +
+ {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} + {}}> + + +
); + }, + ], + globalTypes: { + scheme: { + name: "Scheme", + description: "Select light or dark mode", + defaultValue: "light", + toolbar: { + icon: "mirror", + items: ["light", "dark"], + dynamicTitle: true + } + } + } +}; + +export default preview; diff --git a/apps/admin-x-design-system/.storybook/storybook.css b/apps/admin-x-design-system/.storybook/storybook.css new file mode 100644 index 0000000..1b4aa6f --- /dev/null +++ b/apps/admin-x-design-system/.storybook/storybook.css @@ -0,0 +1,247 @@ +/* + * We load Inter in Ember admin, so loading it explicitly here makes the final rendering + * in Storybook match the final rendering when embedded in Ember + */ +@font-face { + font-family: "Inter"; + src: url("./Inter.ttf") format("truetype-variations"); + font-weight: 100 900; +} + +:root { + font-size: 62.5%; + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +html, body, #root { + width: 100%; + height: 100%; + margin: 0; + letter-spacing: unset; +} + +.sbdocs-wrapper { + padding: 3vmin !important; +} + +.sbdocs-wrapper .sbdocs-content { + max-width: 1320px; +} + +.sb-doc { + max-width: 740px; + width: 100%; + margin: 0 auto !important; +} + +.sb-doc, +.sb-doc a, +.sb-doc h1, +.sb-doc h2, +.sb-doc h3, +.sb-doc h4, +.sb-doc h5, +.sb-doc h6, +.sb-doc p, +.sb-doc ul li, +.sbdocs-title, +.sb-doc ol li { + font-family: Inter, sans-serif !important; + padding: 0 !important; +} + +.sb-doc a { + color: #30CF43; +} + +.sb-doc h1 { + font-size: 48px !important; + letter-spacing: -0.04em !important; + margin-bottom: 20px; +} + +.sb-doc h2 { + margin-top: 40px !important; + font-size: 27px; + border: none; + margin-bottom: 2px; +} + +.sb-doc h3 { + margin-top: 40px !important; + margin-bottom: 4px !important; + font-size: 20px; +} + +.sb-doc h4 { + margin: 0 0 4px !important; +} + +.sb-doc p, +.sb-doc div, +.sb-doc ul li, +.sb-doc ol li { + font-size: 15px; + line-height: 1.5em; +} + +.sb-doc ul li, +.sb-doc ol li { + margin-bottom: 8px; +} + +.sb-doc h2 + p, +.sb-doc h3 + p { + margin-top: 8px; +} + +.sb-doc img, +.sb-wide img { + margin-top: 40px !important; + margin-bottom: 40px !important; +} + +.sb-doc img.small { + max-width: 520px; + margin: 0 auto; + display: block; +} + +.sb-doc p.excerpt { + font-size: 19px; + letter-spacing: -0.02em; +} + +.sb-doc .highlight { + padding: 12px 20px; + border-radius: 4px; + background: #EBEEF0; +} + +.sb-doc .highlight.purple { + background: #F0E9FA; +} + +.sb-doc .highlight.purple a { + color: #8E42FF; +} + +/* Welcome */ +.sb-doc img.main-image { + margin-top: -2vmin !important; + margin-left: -44px; + margin-right: -32px; + margin-bottom: 0 !important; + max-width: unset; + width: calc(100% + 64px); +} + +.sb-doc .main-structure-container { + display: flex; + gap: 32px; + margin: 32px 0 80px; +} + +.sb-doc .main-structure-container div { + flex-basis: 33%; +} + +.sb-doc .main-structure-container div p { + display: flex; + flex-direction: column; + gap: 4px; +} + +.sb-doc .main-structure-container img { + margin: 12px 0 !important; + width: 32px; + height: 32px; +} + +.sb-doc .main-structure-container div h4 { + border-bottom: 1px solid #EBEEF0; + padding-bottom: 8px !important; + margin-bottom: 8px !important; +} + +.sb-doc .main-structure-container div p { + margin: 0; + font-size: 13.5px; +} + +/* Colors */ +.color-grid { + display: flex; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} + +.color-grid div { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px; + border-radius: 4px; + border: 1px solid #EBEEF0; +} + +.color-grid .swatch { + display: block; + background: #EFEFEF; + border-radius: 100%; + width: 28px; + height: 28px; +} + +.swatch.green { + background: #30CF43; +} + +.swatch.black { + background: #15171A; +} + +.swatch.white { + background: #FFFFFF; + border: 1px solid #EBEEF0; +} + +.swatch.lime { + background: #B5FF18; +} +.swatch.blue { + background: #14B8FF; +} +.swatch.purple { + background: #8E42FF; +} +.swatch.pink { + background: #FB2D8D; +} +.swatch.yellow { + background: #FFB41F; +} +.swatch.red { + background: #F50B23; +} + +/* Icons */ + +.sb-doc .streamline { + display: grid; + grid-template-columns: auto 240px; + gap: 32px; +} + +.sbdocs-a { + color: #30CF43 !important; +} \ No newline at end of file diff --git a/apps/admin-x-design-system/README.md b/apps/admin-x-design-system/README.md new file mode 100644 index 0000000..c766d16 --- /dev/null +++ b/apps/admin-x-design-system/README.md @@ -0,0 +1,17 @@ +# Admin X Design + +Components, design guidelines and documentation for building apps in Ghost Admin + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +## Test + +- `pnpm lint` run just eslint +- `pnpm test` run lint and tests + diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json new file mode 100644 index 0000000..4d990a3 --- /dev/null +++ b/apps/admin-x-design-system/package.json @@ -0,0 +1,112 @@ +{ + "name": "@tryghost/admin-x-design-system", + "type": "module", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/admin-x-design-system", + "author": "Ghost Foundation", + "private": true, + "main": "es/index.js", + "types": "types/index.d.ts", + "sideEffects": false, + "scripts": { + "dev": "vite build --watch", + "build": "tsc -p tsconfig.declaration.json && vite build", + "test": "pnpm test:unit", + "test:unit": "pnpm test:types && vitest run", + "test:types": "tsc --noEmit", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache", + "lint": "pnpm lint:code && pnpm lint:test", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "files": [ + "es", + "types" + ], + "devDependencies": { + "@codemirror/lang-html": "6.4.11", + "@codemirror/state": "6.6.0", + "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-tooltip": "1.2.8", + "@storybook/addon-essentials": "8.6.14", + "@storybook/addon-interactions": "8.6.14", + "@storybook/addon-links": "8.6.14", + "@storybook/addon-styling": "1.3.7", + "@storybook/blocks": "8.6.14", + "@storybook/preview-api": "^8.6.14", + "@storybook/react": "8.6.14", + "@storybook/react-vite": "8.6.14", + "@storybook/testing-library": "0.2.2", + "@tailwindcss/postcss": "4.2.1", + "@testing-library/react": "14.3.1", + "@testing-library/react-hooks": "8.0.1", + "@types/lodash-es": "4.17.12", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@types/validator": "13.15.10", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.4.21", + "c8": "10.1.3", + "chai": "4.5.0", + "eslint": "catalog:", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-tailwindcss": "4.0.0-beta.0", + "glob": "^10.5.0", + "jsdom": "28.1.0", + "lodash-es": "4.18.1", + "postcss": "8.5.6", + "postcss-import": "16.1.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "rollup-plugin-node-builtins": "2.1.2", + "sinon": "18.0.1", + "storybook": "8.6.15", + "tailwindcss": "4.2.1", + "typescript": "5.9.3", + "validator": "13.12.0", + "vite": "5.4.21", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + }, + "dependencies": { + "@dnd-kit/core": "6.3.1", + "@dnd-kit/sortable": "7.0.2", + "@ebay/nice-modal-react": "1.2.13", + "@radix-ui/react-avatar": "1.1.11", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-separator": "1.1.8", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-tooltip": "1.2.8", + "@sentry/react": "7.120.4", + "@tryghost/shade": "workspace:*", + "@uiw/react-codemirror": "4.25.2", + "clsx": "2.1.1", + "react-colorful": "5.6.1", + "react-hot-toast": "2.6.0", + "react-select": "5.10.2" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "nx": { + "targets": { + "build": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/admin-x-design-system/postcss.config.cjs b/apps/admin-x-design-system/postcss.config.cjs new file mode 100644 index 0000000..2ca9a4e --- /dev/null +++ b/apps/admin-x-design-system/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + '@tailwindcss/postcss': {}, + autoprefixer: {} + } +}; diff --git a/apps/admin-x-design-system/preflight.css b/apps/admin-x-design-system/preflight.css new file mode 100644 index 0000000..26c65f5 --- /dev/null +++ b/apps/admin-x-design-system/preflight.css @@ -0,0 +1,381 @@ +.admin-x-base { + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) + */ + + *, + ::before, + ::after { + box-sizing: border-box; /* 1 */ + max-width: revert; + max-height: revert; + min-width: revert; + min-height: revert; + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */ + } + + ::before, + ::after { + --tw-content: ''; + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + */ + + html { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + tab-size: 4; /* 3 */ + font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); /* 4 */ + } + + /* + 1. Remove the margin in all browsers. + 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. + */ + + body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Ensure horizontal rules are visible by default. + */ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. + */ + + abbr:where([title]) { + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. + */ + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0; + padding: 0; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. + */ + + a { + color: inherit; + text-decoration: inherit; + } + + /* + Add the correct font weight in Edge and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured `mono` font family by default. + 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp, + pre { + font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); /* 1 */ + font-size: 1em; /* 2 */ + } + + /* + Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. + */ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + margin: 0; + width: auto; + max-width: auto; + } + + table td, table th { + padding: unset; + vertical-align: middle; + text-align: left; + line-height: auto; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + } + + /* + 1. Change the font styles in all browsers. + 2. Remove the margin in Firefox and Safari. + 3. Remove default padding in all browsers. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + font-weight: inherit; /* 1 */ + line-height: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ + outline: none; + } + + /* + Remove the inheritance of text transform in Edge and Firefox. + */ + + button, + select { + text-transform: none; + letter-spacing: inherit; + border-radius: inherit; + appearance: auto; + -webkit-appearance: auto; + background: unset; + } + + /* + 1. Correct the inability to style clickable types in iOS and Safari. + 2. Remove default button styles. + */ + + button, + /* [type='button'], */ + [type='reset'], + [type='submit'] { + -webkit-appearance: button; /* 1 */ + background-color: transparent; /* 2 */ + background-image: none; /* 2 */ + } + + + + /* + Use the modern Firefox focus style for all focusable elements. + */ + + :-moz-focusring { + outline: none; + } + + /* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) + */ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. + */ + + progress { + vertical-align: baseline; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. + */ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + 1. Correct the odd appearance in Chrome and Safari. + 2. Correct the outline style in Safari. + */ + + [type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /* + Remove the inner padding in Chrome and Safari on macOS. + */ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Correct the inability to style clickable types in iOS and Safari. + 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* + Add the correct display in Chrome and Safari. + */ + + summary { + display: list-item; + } + + /* + Removes the default spacing and border for appropriate elements. + */ + + blockquote, + dl, + dd, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + figure, + p, + pre { + margin: 0; + } + + fieldset { + margin: 0; + padding: 0; + } + + legend { + padding: 0; + } + + ol, + ul, + menu { + list-style: none; + margin: 0; + padding: 0; + } + + li { + margin: unset; + line-height: unset; + } + + /* + Prevent resizing textareas horizontally by default. + */ + + textarea { + resize: vertical; + } + + /* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to the user's configured gray 400 color. + */ + + input::placeholder, + textarea::placeholder { + opacity: 1; /* 1 */ + @apply text-grey-500; /* 2 */ + } + + button:focus-visible, + input:focus-visible { + outline: none; + } + + /* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. + */ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) + */ + + img, + video { + max-width: 100%; + height: auto; + } +} diff --git a/apps/admin-x-design-system/src/boilerplate.stories.tsx b/apps/admin-x-design-system/src/boilerplate.stories.tsx new file mode 100644 index 0000000..3706413 --- /dev/null +++ b/apps/admin-x-design-system/src/boilerplate.stories.tsx @@ -0,0 +1,18 @@ +import type {Meta, StoryObj} from '@storybook/react'; + +import BoilerPlate from './boilerplate'; + +const meta = { + title: 'Meta / Boilerplate', + component: BoilerPlate, + tags: ['autodocs'] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'This is a boilerplate component. Use as a basis to create new components.' + } +}; diff --git a/apps/admin-x-design-system/src/boilerplate.tsx b/apps/admin-x-design-system/src/boilerplate.tsx new file mode 100644 index 0000000..fcccdbe --- /dev/null +++ b/apps/admin-x-design-system/src/boilerplate.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +interface BoilerPlateProps { + children?: React.ReactNode; +} + +const BoilerPlate: React.FC = ({children}) => { + return ( + <> + {children} + + ); +}; + +export default BoilerPlate; \ No newline at end of file diff --git a/apps/admin-x-design-system/src/design-system-app.tsx b/apps/admin-x-design-system/src/design-system-app.tsx new file mode 100644 index 0000000..08f226d --- /dev/null +++ b/apps/admin-x-design-system/src/design-system-app.tsx @@ -0,0 +1,26 @@ +import clsx from 'clsx'; +import React from 'react'; +import {FetchKoenigLexical} from './global/form/html-editor'; +import DesignSystemProvider from './providers/design-system-provider'; + +export interface DesignSystemAppProps extends React.HTMLProps { + darkMode: boolean; + fetchKoenigLexical: FetchKoenigLexical; +} + +const DesignSystemApp: React.FC = ({darkMode, fetchKoenigLexical, className, children, ...props}) => { + const appClassName = clsx( + 'admin-x-base', + className + ); + + return ( +
+ + {children} + +
+ ); +}; + +export default DesignSystemApp; diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts new file mode 100644 index 0000000..096c377 --- /dev/null +++ b/apps/admin-x-design-system/src/index.ts @@ -0,0 +1,177 @@ +export {ReactComponent as FacebookLogo} from './assets/images/facebook-logo.svg'; +export {ReactComponent as GhostLogo} from './assets/images/ghost-logo.svg'; +export {ReactComponent as GhostOrb} from './assets/images/ghost-orb.svg'; +export {ReactComponent as GoogleLogo} from './assets/images/google-logo.svg'; +export {ReactComponent as TwitterLogo} from './assets/images/twitter-logo.svg'; +export {ReactComponent as XLogo} from './assets/images/x-logo.svg'; + +export {default as DesktopChrome} from './global/chrome/desktop-chrome'; +export type {DesktopChromeProps} from './global/chrome/desktop-chrome'; +export {default as DesktopChromeHeader} from './global/chrome/desktop-chrome-header'; +export type {DesktopChromeHeaderProps} from './global/chrome/desktop-chrome-header'; +export {default as MobileChrome} from './global/chrome/mobile-chrome'; +export type {MobileChromeProps} from './global/chrome/mobile-chrome'; + +export {default as Checkbox} from './global/form/checkbox'; +export type {CheckboxProps} from './global/form/checkbox'; +export {default as CheckboxGroup} from './global/form/checkbox-group'; +export type {CheckboxGroupProps} from './global/form/checkbox-group'; +export {default as CodeEditor} from './global/form/code-editor'; +export type {CodeEditorProps, FetchKoenigLexical} from './global/form/code-editor'; +export {default as ColorIndicator} from './global/form/color-indicator'; +export type {ColorIndicatorProps} from './global/form/color-indicator'; +export {default as ColorPicker} from './global/form/color-picker'; +export type {ColorPickerProps} from './global/form/color-picker'; +export {default as ColorPickerField} from './global/form/color-picker-field'; +export type {ColorPickerFieldProps} from './global/form/color-picker-field'; +export {default as CurrencyField} from './global/form/currency-field'; +export type {CurrencyFieldProps} from './global/form/currency-field'; +export {default as FileUpload} from './global/form/file-upload'; +export type {FileUploadProps} from './global/form/file-upload'; +export {default as Form} from './global/form/form'; +export type {FormProps} from './global/form/form'; +export {default as HtmlEditor} from './global/form/html-editor'; +export type {HtmlEditorProps} from './global/form/html-editor'; +export {default as HtmlField} from './global/form/html-field'; +export type {HtmlFieldProps} from './global/form/html-field'; +export {default as KoenigEditorBase, loadKoenig} from './global/form/koenig-editor-base'; +export type {KoenigEditorBaseProps, KoenigInstance, NodeType} from './global/form/koenig-editor-base'; +export {default as ImageUpload} from './global/form/image-upload'; +export type {ImageUploadProps} from './global/form/image-upload'; +export {default as MultiSelect} from './global/form/multi-select'; +export type {LoadMultiSelectOptions, MultiSelectOption, MultiSelectProps} from './global/form/multi-select'; +export {default as Radio} from './global/form/radio'; +export type {RadioProps} from './global/form/radio'; +export {default as Select} from './global/form/select'; +export type {LoadSelectOptions, SelectOption, SelectOptionGroup, SelectProps} from './global/form/select'; +export {default as SelectWithOther} from './global/form/select-with-other'; +export type {SelectWithOtherProps} from './global/form/select-with-other'; +export {default as TextArea} from './global/form/text-area'; +export type {TextAreaProps} from './global/form/text-area'; +export {default as TextField} from './global/form/text-field'; +export type {TextFieldProps} from './global/form/text-field'; +export {default as Toggle} from './global/form/toggle'; +export type {ToggleProps} from './global/form/toggle'; +export {default as ToggleGroup} from './global/form/toggle-group'; +export type {ToggleGroupProps} from './global/form/toggle-group'; +export {default as URLTextField} from './global/form/url-text-field'; +export type {URLTextFieldProps} from './global/form/url-text-field'; + +export {default as ConfirmationModal, ConfirmationModalContent} from './global/modal/confirmation-modal'; +export type {ConfirmationModalProps} from './global/modal/confirmation-modal'; +export {default as LimitModal, LimitModalContent} from './global/modal/limit-modal'; +export type {LimitModalProps} from './global/modal/limit-modal'; +export {default as Modal, topLevelBackdropClasses} from './global/modal/modal'; +export type {ModalProps} from './global/modal/modal'; +export {default as ModalPage} from './global/modal/modal-page'; +export type {ModalPageProps} from './global/modal/modal-page'; +export {default as PreviewModal, PreviewModalContent} from './global/modal/preview-modal'; +export type {PreviewModalProps} from './global/modal/preview-modal'; + +export {default as Avatar} from './global/avatar'; +export type {AvatarProps} from './global/avatar'; +export {default as Banner} from './global/banner'; +export type {BannerProps} from './global/banner'; +export {default as Breadcrumbs} from './global/breadcrumbs'; +export type {BreadcrumbItem, BreadcrumbsProps} from './global/breadcrumbs'; +export {default as Button} from './global/button'; +export type {ButtonColor, ButtonProps} from './global/button'; +export {default as ButtonGroup} from './global/button-group'; +export type {ButtonGroupProps} from './global/button-group'; +export {default as ErrorBoundary, withErrorBoundary} from './global/error-boundary'; +export type {ErrorBoundaryProps} from './global/error-boundary'; +export {default as Heading} from './global/heading'; +export type {HeadingProps} from './global/heading'; +export {default as Hint} from './global/hint'; +export type {HintProps} from './global/hint'; +export {default as Icon} from './global/icon'; +export type {IconProps} from './global/icon'; +export {default as IconLabel} from './global/icon-label'; +export type {IconLabelProps} from './global/icon-label'; +export {default as InfiniteScrollListener} from './global/infinite-scroll-listener'; +export type {InfiniteScrollListenerProps} from './global/infinite-scroll-listener'; +export {default as Link} from './global/link'; +export type {LinkProps} from './global/link'; +export {default as List} from './global/list'; +export type {ListProps} from './global/list'; +export {default as ListHeading} from './global/list-heading'; +export type {ListHeadingProps} from './global/list-heading'; +export {default as ListItem} from './global/list-item'; +export type {ListItemProps} from './global/list-item'; +export {LoadingIndicator} from './global/loading-indicator'; +export type {LoadingIndicatorProps} from './global/loading-indicator'; +export {default as Menu} from './global/menu'; +export type {MenuItem, MenuProps} from './global/menu'; +export {default as NoValueLabel} from './global/no-value-label'; +export type {NoValueLabelProps} from './global/no-value-label'; +export {default as Pagination} from './global/pagination'; +export type {PaginationProps} from './global/pagination'; +export {default as Popover} from './global/popover'; +export type {PopoverProps} from './global/popover'; +export {default as Separator} from './global/separator'; +export type {SeparatorProps} from './global/separator'; +export {DragIndicator, default as SortableList} from './global/sortable-list'; +export type {DragIndicatorProps, SortableItemContainerProps, SortableListProps} from './global/sortable-list'; +export {default as SortMenu} from './global/sort-menu'; +export type {SortMenuProps} from './global/sort-menu'; +export {default as StickyFooter} from './global/sticky-footer'; +export type {StickyFooterProps} from './global/sticky-footer'; +export {default as TabView} from './global/tab-view'; +export type {Tab, TabViewProps} from './global/tab-view'; +export {default as Table} from './global/table'; +export type {ShowMoreData, TableProps} from './global/table'; +export {default as TableCell} from './global/table-cell'; +export type {TableCellProps} from './global/table-cell'; +export {default as TableHead} from './global/table-head'; +export type {TableHeadProps} from './global/table-head'; +export {default as TableRow} from './global/table-row'; +export type {TableRowProps} from './global/table-row'; +export {default as Toast, dismissAllToasts, showToast} from './global/toast'; +export type {ToastProps} from './global/toast'; +export {default as Tooltip} from './global/tooltip'; +export type {TooltipProps} from './global/tooltip'; +export {default as PageHeader} from './global/layout/page-header'; +export type {PageHeaderProps} from './global/layout/page-header'; +export {default as Page} from './global/layout/page'; +export type {PageTab} from './global/layout/page'; +export type {CustomGlobalAction} from './global/layout/page'; +export {default as ViewContainer} from './global/layout/view-container'; +export type {View} from './global/layout/view-container'; +export type {ViewTab} from './global/layout/view-container'; +export type {PrimaryActionProps} from './global/layout/view-container'; +export {default as DynamicTable} from './global/table/dynamic-table'; +export type {DynamicTableProps} from './global/table/dynamic-table'; +export type {DynamicTableColumn} from './global/table/dynamic-table'; +export type {DynamicTableRow} from './global/table/dynamic-table'; + +export {default as SettingGroup} from './settings/setting-group'; +export type {SettingGroupProps} from './settings/setting-group'; +export {default as SettingGroupContent} from './settings/setting-group-content'; +export type {SettingGroupContentProps} from './settings/setting-group-content'; +export {default as SettingGroupHeader} from './settings/setting-group-header'; +export type {SettingGroupHeaderProps} from './settings/setting-group-header'; +export {default as SettingNavItem} from './settings/setting-nav-item'; +export type {SettingNavItemProps} from './settings/setting-nav-item'; +export {default as SettingNavSection} from './settings/setting-nav-section'; +export type {SettingNavSectionProps} from './settings/setting-nav-section'; +export {default as SettingSection} from './settings/setting-section'; +export type {SettingSectionProps} from './settings/setting-section'; +export {default as SettingSectionHeader} from './settings/setting-section-header'; +export type {SettingSectionHeaderProps} from './settings/setting-section-header'; +export {default as SettingValue} from './settings/setting-value'; +export type {SettingValueProps} from './settings/setting-value'; +export {default as StripeButton} from './settings/stripe-button'; +export type {StripeButtonProps} from './settings/stripe-button'; + +export {default as useGlobalDirtyState} from './hooks/use-global-dirty-state'; +export {usePagination} from './hooks/use-pagination'; +export type {PaginationData} from './hooks/use-pagination'; +export {default as useSortableIndexedList} from './hooks/use-sortable-indexed-list'; + +export {debounce} from './utils/debounce'; +export {confirmIfDirty} from './utils/modals'; +export {formatUrl} from './utils/format-url'; + +export {default as DesignSystemApp} from './design-system-app'; +export type {DesignSystemAppProps} from './design-system-app'; +export {useFocusContext, useDesignSystem} from './providers/design-system-provider'; diff --git a/apps/admin-x-design-system/src/typings.d.ts b/apps/admin-x-design-system/src/typings.d.ts new file mode 100644 index 0000000..d733c0d --- /dev/null +++ b/apps/admin-x-design-system/src/typings.d.ts @@ -0,0 +1,7 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; +} diff --git a/apps/admin-x-design-system/styles.base.css b/apps/admin-x-design-system/styles.base.css new file mode 100644 index 0000000..0300e48 --- /dev/null +++ b/apps/admin-x-design-system/styles.base.css @@ -0,0 +1,122 @@ +@import './preflight.css'; + +@import 'tailwindcss/theme.css'; +@import '@tryghost/shade/tailwind.theme.css'; + +@import url(https://fonts.bunny.net/css?family=cardo:400,700); +@import url(https://fonts.bunny.net/css?family=manrope:300,500,700); +@import url(https://fonts.bunny.net/css?family=merriweather:300,700); +@import url(https://fonts.bunny.net/css?family=nunito:400,600,700); +@import url(https://fonts.bunny.net/css?family=old-standard-tt:400,700); +@import url(https://fonts.bunny.net/css?family=prata:400); +@import url(https://fonts.bunny.net/css?family=roboto:400,500,700); +@import url(https://fonts.bunny.net/css?family=rufina:400,500,700); +@import url(https://fonts.bunny.net/css?family=tenor-sans:400); +@import url(https://fonts.bunny.net/css?family=space-grotesk:700); +@import url(https://fonts.bunny.net/css?family=chakra-petch:400); +@import url(https://fonts.bunny.net/css?family=noto-sans:400,700); +@import url(https://fonts.bunny.net/css?family=poppins:400,700); +@import url(https://fonts.bunny.net/css?family=fira-sans:400,700); +@import url(https://fonts.bunny.net/css?family=inter:400,700); +@import url(https://fonts.bunny.net/css?family=noto-serif:400,700); +@import url(https://fonts.bunny.net/css?family=lora:400,700); +@import url(https://fonts.bunny.net/css?family=ibm-plex-serif:400,700); +@import url(https://fonts.bunny.net/css?family=space-mono:400,700); +@import url(https://fonts.bunny.net/css?family=fira-mono:400,700); +@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,700); + +/* Defaults */ +@layer base { + /* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */ + @font-face { + font-family: "Inter"; + src: local("Inter") format("truetype-variations"); + font-weight: 100 900; + } + + .admin-x-base { + & { + @apply font-sans text-black text-base leading-normal; + } + + h1, h2, h3, h4, h5 { + @apply font-bold tracking-tight leading-tighter; + } + + h1 { + @apply text-4xl leading-supertight; + } + + h2 { + @apply text-2xl; + } + + h3 { + @apply text-xl; + } + + h4 { + @apply text-lg; + } + + h5 { + @apply text-md leading-supertight; + } + + h6 { + @apply text-md leading-normal; + } + } +} + +.admin-x-base { + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + letter-spacing: unset; + + height: 100vh; + width: 100%; + overflow-x: hidden; + overflow-y: auto; +} + + +/* +Used to be for fixed bottom mobile menu bar +@media (max-width: 800px) { + .admin-x-base { + height: calc(100vh - 55px); + } +} */ + +.dark .admin-x-base { + color: #FAFAFB; +} + +.dark .admin-x-base .gh-loading-orb-container { + background-color: #000000; +} + +.dark .admin-x-base .gh-loading-orb { + filter: invert(100%); +} + +.admin-x-base .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome */ +} + +.admin-x-base .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Prose classes are for formatting arbitrary HTML that comes from the API */ +.gh-prose-links a { + color: #30CF43; +} diff --git a/apps/admin-x-design-system/styles.css b/apps/admin-x-design-system/styles.css new file mode 100644 index 0000000..68ad7b0 --- /dev/null +++ b/apps/admin-x-design-system/styles.css @@ -0,0 +1,123 @@ +@import './preflight.css'; + +@import 'tailwindcss/theme.css'; +@import '@tryghost/shade/tailwind.theme.css'; +@import 'tailwindcss/utilities.css'; + +@import url(https://fonts.bunny.net/css?family=cardo:400,700); +@import url(https://fonts.bunny.net/css?family=manrope:300,500,700); +@import url(https://fonts.bunny.net/css?family=merriweather:300,700); +@import url(https://fonts.bunny.net/css?family=nunito:400,600,700); +@import url(https://fonts.bunny.net/css?family=old-standard-tt:400,700); +@import url(https://fonts.bunny.net/css?family=prata:400); +@import url(https://fonts.bunny.net/css?family=roboto:400,500,700); +@import url(https://fonts.bunny.net/css?family=rufina:400,500,700); +@import url(https://fonts.bunny.net/css?family=tenor-sans:400); +@import url(https://fonts.bunny.net/css?family=space-grotesk:700); +@import url(https://fonts.bunny.net/css?family=chakra-petch:400); +@import url(https://fonts.bunny.net/css?family=noto-sans:400,700); +@import url(https://fonts.bunny.net/css?family=poppins:400,700); +@import url(https://fonts.bunny.net/css?family=fira-sans:400,700); +@import url(https://fonts.bunny.net/css?family=inter:400,700); +@import url(https://fonts.bunny.net/css?family=noto-serif:400,700); +@import url(https://fonts.bunny.net/css?family=lora:400,700); +@import url(https://fonts.bunny.net/css?family=ibm-plex-serif:400,700); +@import url(https://fonts.bunny.net/css?family=space-mono:400,700); +@import url(https://fonts.bunny.net/css?family=fira-mono:400,700); +@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,700); + +/* Defaults */ +@layer base { + /* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */ + @font-face { + font-family: "Inter"; + src: local("Inter") format("truetype-variations"); + font-weight: 100 900; + } + + .admin-x-base { + & { + @apply font-sans text-black text-base leading-normal; + } + + h1, h2, h3, h4, h5 { + @apply font-bold tracking-tight leading-tighter; + } + + h1 { + @apply text-4xl leading-supertight; + } + + h2 { + @apply text-2xl; + } + + h3 { + @apply text-xl; + } + + h4 { + @apply text-lg; + } + + h5 { + @apply text-md leading-supertight; + } + + h6 { + @apply text-md leading-normal; + } + } +} + +.admin-x-base { + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + letter-spacing: unset; + + height: 100vh; + width: 100%; + overflow-x: hidden; + overflow-y: auto; +} + + +/* +Used to be for fixed bottom mobile menu bar +@media (max-width: 800px) { + .admin-x-base { + height: calc(100vh - 55px); + } +} */ + +.dark .admin-x-base { + color: #FAFAFB; +} + +.dark .admin-x-base .gh-loading-orb-container { + background-color: #000000; +} + +.dark .admin-x-base .gh-loading-orb { + filter: invert(100%); +} + +.admin-x-base .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome */ +} + +.admin-x-base .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Prose classes are for formatting arbitrary HTML that comes from the API */ +.gh-prose-links a { + color: #30CF43; +} diff --git a/apps/admin-x-design-system/test/.eslintrc.cjs b/apps/admin-x-design-system/test/.eslintrc.cjs new file mode 100644 index 0000000..03a6897 --- /dev/null +++ b/apps/admin-x-design-system/test/.eslintrc.cjs @@ -0,0 +1,11 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ], + rules: { + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/apps/admin-x-design-system/tsconfig.declaration.json b/apps/admin-x-design-system/tsconfig.declaration.json new file mode 100644 index 0000000..c7b87e9 --- /dev/null +++ b/apps/admin-x-design-system/tsconfig.declaration.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/admin-x-design-system/tsconfig.json b/apps/admin-x-design-system/tsconfig.json new file mode 100644 index 0000000..e357441 --- /dev/null +++ b/apps/admin-x-design-system/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] + } diff --git a/apps/admin-x-design-system/tsconfig.node.json b/apps/admin-x-design-system/tsconfig.node.json new file mode 100644 index 0000000..364bc0e --- /dev/null +++ b/apps/admin-x-design-system/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "package.json"] +} diff --git a/apps/admin-x-design-system/vite.config.ts b/apps/admin-x-design-system/vite.config.ts new file mode 100644 index 0000000..a30a739 --- /dev/null +++ b/apps/admin-x-design-system/vite.config.ts @@ -0,0 +1,67 @@ +import react from '@vitejs/plugin-react'; +import {globSync} from 'glob'; +import {resolve} from 'path'; +import svgr from 'vite-plugin-svgr'; +import {defineConfig} from 'vitest/config'; + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + svgr(), + react() + ], + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.VITEST_SEGFAULT_RETRY': 3 + }, + preview: { + port: 4174 + }, + build: { + reportCompressedSize: false, + minify: false, + sourcemap: true, + outDir: 'es', + lib: { + formats: ['es'], + entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, path) => { + if (path.includes('.stories.') || path.endsWith('.d.ts')) { + return entries; + } + + const outPath = path.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, ''); + entries[outPath] = path; + return entries; + }, {} as Record) + }, + commonjsOptions: { + include: [/packages/, /node_modules/] + }, + rollupOptions: { + external: (source) => { + if (source.startsWith('.')) { + return false; + } + + if (source.includes('node_modules')) { + return true; + } + + return !source.includes(__dirname); + } + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + include: ['./test/unit/**/*'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); +}); diff --git a/apps/admin-x-framework/.eslintrc.cjs b/apps/admin-x-framework/.eslintrc.cjs new file mode 100644 index 0000000..9c75dd8 --- /dev/null +++ b/apps/admin-x-framework/.eslintrc.cjs @@ -0,0 +1,42 @@ +module.exports = { + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + } + }, + rules: { + // suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + 'no-restricted-imports': ['error', { + paths: [{ + name: '@tryghost/shade', + message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).' + }] + }], + + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/apps/admin-x-framework/.gitignore b/apps/admin-x-framework/.gitignore new file mode 100644 index 0000000..7998256 --- /dev/null +++ b/apps/admin-x-framework/.gitignore @@ -0,0 +1,2 @@ +dist +types diff --git a/apps/admin-x-framework/README.md b/apps/admin-x-framework/README.md new file mode 100644 index 0000000..065a3c9 --- /dev/null +++ b/apps/admin-x-framework/README.md @@ -0,0 +1,22 @@ +# Admin X Framework + +Ghost Shared Framework that is used by all the micro-frontends for common functionality like data fetching and routing. + +## Pre-requisites + +- Run `pnpm` in Ghost monorepo root + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +## Test + +- `pnpm lint` - run just eslint +- `pnpm test` - runs acceptance tests + +In package.json you can find other related running options too. diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json new file mode 100644 index 0000000..6daaab9 --- /dev/null +++ b/apps/admin-x-framework/package.json @@ -0,0 +1,128 @@ +{ + "name": "@tryghost/admin-x-framework", + "type": "module", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/apps/admin-x-framework", + "author": "Ghost Foundation", + "private": true, + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./types/index.d.ts" + }, + "./errors": { + "import": "./dist/errors.js", + "require": "./dist/errors.cjs", + "types": "./types/errors.d.ts" + }, + "./helpers": { + "import": "./dist/helpers.js", + "require": "./dist/helpers.cjs", + "types": "./types/helpers.d.ts" + }, + "./hooks": { + "import": "./dist/hooks.js", + "require": "./dist/hooks.cjs", + "types": "./types/hooks.d.ts" + }, + "./routing": { + "import": "./dist/routing.js", + "require": "./dist/routing.cjs", + "types": "./types/routing.d.ts" + }, + "./api/*": { + "import": "./dist/api/*.js", + "require": "./dist/api/*.cjs", + "types": "./types/api/*.d.ts" + }, + "./utils/post-utils": { + "import": "./dist/utils/post-utils.js", + "require": "./dist/utils/post-utils.cjs", + "types": "./types/utils/post-utils.d.ts" + }, + "./vite": { + "import": "./dist/vite.js", + "require": "./dist/vite.cjs", + "types": "./types/vite.d.ts" + }, + "./playwright": { + "import": "./dist/playwright.js", + "require": "./dist/playwright.cjs", + "types": "./types/playwright.d.ts" + }, + "./test/*": { + "import": "./dist/test/*.js", + "require": "./dist/test/*.cjs", + "types": "./types/test/*.d.ts" + } + }, + "sideEffects": false, + "scripts": { + "dev": "vite build --watch", + "build": "tsc -p tsconfig.declaration.json && vite build", + "test": "pnpm test:types && pnpm test:unit", + "test:types": "tsc --noEmit", + "test:unit": "vitest run --coverage", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache", + "lint": "pnpm lint:code && pnpm lint:test", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache" + }, + "files": [ + "dist", + "types" + ], + "devDependencies": { + "@playwright/test": "1.59.1", + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "14.3.1", + "@tryghost/koenig-lexical": "1.7.30", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "^1.6.1", + "c8": "10.1.3", + "eslint": "catalog:", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "glob": "^10.5.0", + "jsdom": "28.1.0", + "msw": "2.12.14", + "sinon": "18.0.1", + "typescript": "5.9.3", + "vite": "5.4.21", + "vite-plugin-css-injected-by-js": "3.5.2", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + }, + "dependencies": { + "@ebay/nice-modal-react": "1.2.13", + "@sentry/react": "7.120.4", + "@tanstack/react-query": "4.36.1", + "@tinybirdco/charts": "0.2.4", + "@tryghost/admin-x-design-system": "workspace:*", + "@tryghost/shade": "workspace:*", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-hot-toast": "2.6.0", + "react-router": "7.14.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "nx": { + "targets": { + "build": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/admin-x-framework/src/errors.ts b/apps/admin-x-framework/src/errors.ts new file mode 100644 index 0000000..1398926 --- /dev/null +++ b/apps/admin-x-framework/src/errors.ts @@ -0,0 +1,2 @@ +export * from './utils/errors'; + diff --git a/apps/admin-x-framework/src/helpers.ts b/apps/admin-x-framework/src/helpers.ts new file mode 100644 index 0000000..dea77bd --- /dev/null +++ b/apps/admin-x-framework/src/helpers.ts @@ -0,0 +1,2 @@ +export * from './utils/helpers'; + diff --git a/apps/admin-x-framework/src/hooks.ts b/apps/admin-x-framework/src/hooks.ts new file mode 100644 index 0000000..0e673ca --- /dev/null +++ b/apps/admin-x-framework/src/hooks.ts @@ -0,0 +1,10 @@ +export {default as useFilterableApi} from './hooks/use-filterable-api'; +export {default as useForm} from './hooks/use-form'; +export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form'; +export {default as useHandleError} from './hooks/use-handle-error'; +export {usePermission} from './hooks/use-permissions'; +export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload'; +export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed'; +export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload'; +export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions'; +export {usePinturaConfig} from './hooks/use-pintura-config'; diff --git a/apps/admin-x-framework/src/index.ts b/apps/admin-x-framework/src/index.ts new file mode 100644 index 0000000..925d62f --- /dev/null +++ b/apps/admin-x-framework/src/index.ts @@ -0,0 +1,58 @@ +// Framework +export type {StatsConfig, FrameworkContextType, FrameworkProviderProps, TopLevelFrameworkProps} from './providers/framework-provider'; +export {FrameworkProvider, useFramework} from './providers/framework-provider'; + +// App Context +export type {AppSettings, BaseAppProps, AppContextType, AppProviderProps} from './providers/app-provider'; +export {AppContext, AppProvider, useAppContext} from './providers/app-provider'; + +// Hooks +export {useActiveVisitors} from './hooks/use-active-visitors'; +export {default as useForm} from './hooks/use-form'; +export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/use-form'; +export {default as useHandleError} from './hooks/use-handle-error'; +export {default as useFilterableApi} from './hooks/use-filterable-api'; +export {useTinybirdToken} from './hooks/use-tinybird-token'; +export type {UseTinybirdTokenResult} from './hooks/use-tinybird-token'; +export {useTinybirdQuery} from './hooks/use-tinybird-query'; +export type {UseTinybirdQueryOptions} from './hooks/use-tinybird-query'; +export {useKoenigFileUpload, koenigFileUploadTypes} from './hooks/use-koenig-file-upload'; +export {useKoenigFetchEmbed} from './hooks/use-koenig-fetch-embed'; +export type {KoenigFileUploadType} from './hooks/use-koenig-file-upload'; +export {useKoenigLinkSuggestions} from './hooks/use-koenig-link-suggestions'; + +// Currency utilities +export {getSymbol} from './utils/currency'; + +// Stats utilities +export {getStatEndpointUrl, getToken} from './utils/stats-config'; + +// Post utilities +export type {Post} from './api/posts'; +export {hasBeenEmailed} from './utils/post-utils'; +export {isEmailOnly, isPublishedOnly, isPublishedAndEmailed, getPostMetricsToDisplay} from './utils/post-helpers'; +export {focusKoenigEditorOnBottomClick} from './utils/focus-koenig-editor-on-bottom-click'; + +// Source utilities +export {SOURCE_DOMAIN_MAP, getFaviconDomain, extractDomain, isDomainOrSubdomain, processSources, extendSourcesWithPercentages, normalizeSource} from './utils/source-utils'; +export type {BaseSourceData, ProcessedSourceData, ExtendSourcesOptions} from './utils/source-utils'; + +// Routing +export type {RouteObject} from 'react-router'; +export type {RouterProviderProps, NavigateOptions} from './providers/router-provider'; +export {RouterProvider, useNavigate, useBaseRoute, useRouteHasParams, resetScrollPosition, ScrollRestoration, Navigate} from './providers/router-provider'; +export {useNavigationStack} from './providers/navigation-stack-provider'; +export {Link, NavLink, Outlet, useLocation, useParams, useSearchParams, redirect, matchRoutes, matchPath, useMatch, useMatches} from 'react-router'; + +// Lazy component loader +export {lazyComponent} from './utils/lazy-component'; + +// Data fetching +export type {InfiniteData} from '@tanstack/react-query'; +export {useQueryClient} from '@tanstack/react-query'; + +// API +export type {TinybirdToken, TinybirdTokenResponseType} from './api/tinybird'; +export {getTinybirdToken} from './api/tinybird'; +export type {FeaturebaseToken, FeaturebaseTokenResponseType} from './api/featurebase'; +export {getFeaturebaseToken} from './api/featurebase'; diff --git a/apps/admin-x-framework/src/playwright.ts b/apps/admin-x-framework/src/playwright.ts new file mode 100644 index 0000000..e668c47 --- /dev/null +++ b/apps/admin-x-framework/src/playwright.ts @@ -0,0 +1,62 @@ +import {defineConfig, devices, PlaywrightTestConfig} from '@playwright/test'; + +export const E2E_PORT = 5173; + +export function adminXPlaywrightConfig(overrides: Partial = {}) { + /** + * See https://playwright.dev/docs/test-configuration. + */ + return defineConfig({ + testDir: './test/acceptance', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Hardcode to use all cores in CI */ + workers: process.env.CI ? '100%' : (process.env.PLAYWRIGHT_SLOWMO ? 1 : undefined), + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: `http://localhost:${E2E_PORT}`, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + launchOptions: { + slowMo: parseInt(process.env.PLAYWRIGHT_SLOWMO ?? '') || 0, + // force GPU hardware acceleration + // (even in headless mode) + args: ['--use-gl=egl'] + } + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']} + }, + + ...(process.env.ALL_BROWSERS ? [{ + name: 'firefox', + use: {...devices['Desktop Firefox']} + }, + + { + name: 'webkit', + use: {...devices['Desktop Safari']} + }] : []) + ], + + /* Run local dev server before starting the tests */ + webServer: { + command: `pnpm dev:start`, + url: `http://localhost:${E2E_PORT}`, + reuseExistingServer: !process.env.CI, + timeout: 10000 + }, + + ...overrides + }); +} diff --git a/apps/admin-x-framework/src/routing.ts b/apps/admin-x-framework/src/routing.ts new file mode 100644 index 0000000..6b66196 --- /dev/null +++ b/apps/admin-x-framework/src/routing.ts @@ -0,0 +1,3 @@ +export {RoutingProvider, useRouteChangeCallback, useRouting} from './providers/routing-provider'; +export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/routing-provider'; + diff --git a/apps/admin-x-framework/src/vite.ts b/apps/admin-x-framework/src/vite.ts new file mode 100644 index 0000000..140aab5 --- /dev/null +++ b/apps/admin-x-framework/src/vite.ts @@ -0,0 +1,88 @@ +import react from '@vitejs/plugin-react'; +import {PluginOption, UserConfig, mergeConfig} from 'vite'; +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import svgr from 'vite-plugin-svgr'; +import {defineConfig} from 'vitest/config'; + +const externalPlugin = ({externals}: { externals: Record }): PluginOption => { + return { + name: 'external-globals', + apply: 'build', + enforce: 'pre', + resolveId(id) { + if (Object.keys(externals).includes(id)) { + // Naming convention for IDs that will be resolved by a plugin + return `\0${id}`; + } + }, + async load(id) { + const [originalId, externalName] = Object.entries(externals).find(([key]) => id === `\0${key}`) || []; + + if (originalId) { + const module = await import(originalId); + + return Object.keys(module).map(key => (key === 'default' ? `export default ${externalName};` : `export const ${key} = ${externalName}.${key};`)).join('\n'); + } + } + }; +}; + +// https://vitejs.dev/config/ +export default function adminXViteConfig({packageName, entry, overrides}: {packageName: string; entry: string; overrides?: UserConfig}) { + const outputFileName = packageName[0] === '@' ? packageName.slice(packageName.indexOf('/') + 1) : packageName; + + const defaultConfig = defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + svgr(), + react(), + externalPlugin({ + externals: { + react: 'React', + 'react-dom': 'ReactDOM' + } + }), + cssInjectedByJsPlugin() as PluginOption // Cast to avoid type conflicts + ], + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.VITEST_SEGFAULT_RETRY': 3, + 'import.meta.env.GHOST_BUILD_VERSION': JSON.stringify(process.env.GHOST_BUILD_VERSION || '') + }, + preview: { + port: 4174 + }, + build: { + reportCompressedSize: false, + minify: true, + sourcemap: true, + lib: { + formats: ['es'], + entry, + name: packageName, + fileName(format) { + if (format === 'umd') { + return `${outputFileName}.umd.js`; + } + + return `${outputFileName}.js`; + } + }, + commonjsOptions: { + include: [/packages/, /node_modules/] + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + include: ['./test/unit/**/*'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); + + return mergeConfig(defaultConfig, overrides || {}); +}; diff --git a/apps/admin-x-framework/test/.eslintrc.cjs b/apps/admin-x-framework/test/.eslintrc.cjs new file mode 100644 index 0000000..dbfb855 --- /dev/null +++ b/apps/admin-x-framework/test/.eslintrc.cjs @@ -0,0 +1,12 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/ts-test' + ], + rules: { + 'ghost/mocha/no-mocha-arrows': 'off', + '@typescript-eslint/no-explicit-any': 'off', + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/apps/admin-x-framework/test/setup.ts b/apps/admin-x-framework/test/setup.ts new file mode 100644 index 0000000..50fa4d0 --- /dev/null +++ b/apps/admin-x-framework/test/setup.ts @@ -0,0 +1,4 @@ +/// +import '@testing-library/jest-dom'; + +// This file ensures TypeScript knows about vitest globals \ No newline at end of file diff --git a/apps/admin-x-framework/tsconfig.declaration.json b/apps/admin-x-framework/tsconfig.declaration.json new file mode 100644 index 0000000..c7b87e9 --- /dev/null +++ b/apps/admin-x-framework/tsconfig.declaration.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/admin-x-framework/tsconfig.json b/apps/admin-x-framework/tsconfig.json new file mode 100644 index 0000000..fc77b08 --- /dev/null +++ b/apps/admin-x-framework/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "test"], + "references": [{ "path": "./tsconfig.node.json" }] + } diff --git a/apps/admin-x-framework/tsconfig.node.json b/apps/admin-x-framework/tsconfig.node.json new file mode 100644 index 0000000..364bc0e --- /dev/null +++ b/apps/admin-x-framework/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "package.json"] +} diff --git a/apps/admin-x-framework/vite.config.ts b/apps/admin-x-framework/vite.config.ts new file mode 100644 index 0000000..32c16c0 --- /dev/null +++ b/apps/admin-x-framework/vite.config.ts @@ -0,0 +1,68 @@ +import path from 'path'; +import react from '@vitejs/plugin-react'; +import {globSync} from 'glob'; +import {resolve} from 'path'; +import {defineConfig} from 'vitest/config'; + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + react() + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, + preview: { + port: 4174 + }, + build: { + reportCompressedSize: false, + minify: false, + sourcemap: true, + outDir: 'dist', + lib: { + formats: ['es', 'cjs'], + entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, libpath) => { + if (libpath.endsWith('.d.ts')) { + return entries; + } + + const outPath = libpath.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, ''); + entries[outPath] = libpath; + return entries; + }, {} as Record) + }, + commonjsOptions: { + include: [/packages/, /node_modules/] + }, + rollupOptions: { + external: (source) => { + if (source.startsWith('.')) { + return false; + } + + if (source.includes('node_modules')) { + return true; + } + + return !source.includes(__dirname); + } + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + include: ['./test/unit/**/*'], + setupFiles: ['./test/setup.ts'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); +}); diff --git a/apps/admin-x-settings/.eslintignore b/apps/admin-x-settings/.eslintignore new file mode 100644 index 0000000..9944ecc --- /dev/null +++ b/apps/admin-x-settings/.eslintignore @@ -0,0 +1 @@ +tailwind.config.cjs diff --git a/apps/admin-x-settings/.eslintrc.cjs b/apps/admin-x-settings/.eslintrc.cjs new file mode 100644 index 0000000..e19dc83 --- /dev/null +++ b/apps/admin-x-settings/.eslintrc.cjs @@ -0,0 +1,159 @@ +/* eslint-env node */ +const tailwindCssConfig = `${__dirname}/../admin/src/index.css`; + +module.exports = { + root: true, + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + }, + tailwindcss: { + config: tailwindCssConfig + } + }, + rules: { + // ---------------------- + // Rules COPIED from base config, remove these when the config is fixed + + // Style Rules + // Require 4 spaces + indent: ['error', 4], + // Require single quotes for strings & properties (allows template literals) + quotes: ['error', 'single', {allowTemplateLiterals: true}], + 'quote-props': ['error', 'as-needed'], + // Require semi colons, always at the end of a line + semi: ['error', 'always'], + 'semi-style': ['error', 'last'], + // Don't allow dangling commas + 'comma-dangle': ['error', 'never'], + // Always require curly braces, and position them at the end and beginning of lines + curly: 'error', + 'brace-style': ['error', '1tbs'], + // Don't allow padding inside of blocks + 'padded-blocks': ['error', 'never'], + // Require objects to be consistently formatted with newlines + 'object-curly-newline': ['error', {consistent: true}], + // Don't allow more than 1 consecutive empty line or an empty 1st line + 'no-multiple-empty-lines': ['error', {max: 1, maxBOF: 0}], + // Variables must be camelcase, but properties are not checked + camelcase: ['error', {properties: 'never'}], + // Allow newlines before dots, not after e.g. .then goes on a new line + 'dot-location': ['error', 'property'], + // Prefer dot notation over array notation + 'dot-notation': ['error'], + + // Spacing rules + // Don't allow multiple spaces anywhere + 'no-multi-spaces': 'error', + // Anonymous functions have a sape, named functions never do + 'space-before-function-paren': ['error', {anonymous: 'always', named: 'never'}], + // Don't put spaces inside of objects or arrays + 'object-curly-spacing': ['error', 'never'], + 'array-bracket-spacing': ['error', 'never'], + // Allow a max of one space between colons and values + 'key-spacing': ['error', {mode: 'strict'}], + // Require spaces before and after keywords like if, else, try, catch etc + 'keyword-spacing': 'error', + // No spaces around semis + 'semi-spacing': 'error', + // 1 space around arrows + 'arrow-spacing': 'error', + // Don't allow spaces inside parenthesis + 'space-in-parens': ['error', 'never'], + // Require single spaces either side of operators + 'space-unary-ops': 'error', + 'space-infix-ops': 'error', + + // Best practice rules + // Require === / !== + eqeqeq: ['error', 'always'], + // Don't allow ++ and -- + 'no-plusplus': ['error', {allowForLoopAfterthoughts: true}], + // Don't allow eval + 'no-eval': 'error', + // Throw errors for unnecessary usage of .call or .apply + 'no-useless-call': 'error', + // Don't allow console.* calls + 'no-console': 'error', + // Prevent [variable shadowing](https://en.wikipedia.org/wiki/Variable_shadowing) + 'no-shadow': ['error'], + + // Return rules + // Prevent missing return statements in array functions like map & reduce + 'array-callback-return': 'error', + 'no-constructor-return': 'error', + 'no-promise-executor-return': 'error', + + // Arrow function styles + // Do not enforce single lines when using arrow functions. + // https://eslint.org/docs/rules/arrow-body-style + 'arrow-body-style': 'off', + 'arrow-parens': ['error', 'as-needed', {requireForBlockBody: true}], + 'implicit-arrow-linebreak': 'error', + 'no-confusing-arrow': 'error', + + // ---------------------- + // Rules NOT COPIED from base config, keep these + + // sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + 'no-restricted-imports': ['error', { + paths: [{ + name: '@tryghost/shade', + message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).' + }] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // TODO: enable this when we have the time to retroactively go and fix the issues + 'prefer-const': 'off', + + // TODO: re-enable this (maybe fixed fast refresh?) + 'react-refresh/only-export-components': 'off', + + // suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + + // TODO: re-enable this because otherwise we're just skirting TS + '@typescript-eslint/no-explicit-any': 'warn', + + // TODO: re-enable these if deemed useful + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-function': 'off', + + // custom react rules + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/enforces-negative-arbitrary-values': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/migration-from-tailwind-2': 'warn', + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': 'error' + } +}; diff --git a/apps/admin-x-settings/README.md b/apps/admin-x-settings/README.md new file mode 100644 index 0000000..07bb5b8 --- /dev/null +++ b/apps/admin-x-settings/README.md @@ -0,0 +1,33 @@ +# Admin X Settings + +Ghost Admin Settings micro-frontend. + +## Pre-requisites + +- Run `pnpm` in Ghost monorepo root + +## Running the app + +### Running the development version + +Run `pnpm dev` (in this package folder) to start the development server to test/develop the settings standalone. This will generate a demo site from the `index.html` file which renders the app and makes it available on http://localhost:5173 + +### Running inside Admin + +Run `pnpm dev` from the top-level repo. This starts all frontend apps via Docker backend + host dev servers, and AdminX will automatically rebuild when you make changes. + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +## Test + +- `pnpm lint` - run just eslint +- `pnpm test:acceptance` - runs acceptance tests +- `pnpm test:unit` - runs unit tests +- `pnpm test:acceptance path/to/test` - runs a specific test +- `pnpm test:acceptance:slowmo` - runs acceptance tests in slow motion and headed mode, useful for debugging and developing tests diff --git a/apps/admin-x-settings/index.html b/apps/admin-x-settings/index.html new file mode 100644 index 0000000..2a045f8 --- /dev/null +++ b/apps/admin-x-settings/index.html @@ -0,0 +1,13 @@ + + + + + + + Settings - Admin + + +
+ + + diff --git a/apps/admin-x-settings/node-shim.cjs b/apps/admin-x-settings/node-shim.cjs new file mode 100644 index 0000000..3f40d5a --- /dev/null +++ b/apps/admin-x-settings/node-shim.cjs @@ -0,0 +1,4 @@ +/** + * This is used by vite to resolve node builtins. See resolve.alias in vite.config.js + */ +module.exports = {}; diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json new file mode 100644 index 0000000..f8abde3 --- /dev/null +++ b/apps/admin-x-settings/package.json @@ -0,0 +1,103 @@ +{ + "name": "@tryghost/admin-x-settings", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TryGhost/Ghost/tree/main/packages/admin-x-settings" + }, + "author": "Ghost Foundation", + "files": [ + "LICENSE", + "README.md", + "dist/" + ], + "main": "./dist/admin-x-settings.umd.cjs", + "module": "./dist/admin-x-settings.js", + "exports": { + ".": { + "import": "./dist/admin-x-settings.js", + "require": "./dist/admin-x-settings.umd.cjs", + "types": "./src/index.tsx" + }, + "./src/*": { + "import": "./src/*.tsx", + "require": "./src/*.tsx" + } + }, + "private": true, + "scripts": { + "dev": "vite build --watch", + "dev:start": "vite", + "build": "tsc && vite build", + "lint": "pnpm run lint:js", + "lint:js": "eslint --ext .js,.ts,.cjs,.tsx --cache src test", + "test": "pnpm test:unit", + "test:unit": "vitest run --config vitest.config.ts", + "test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' playwright test", + "test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance --headed", + "test:acceptance:full": "ALL_BROWSERS=1 pnpm test:acceptance", + "preview": "vite preview" + }, + "dependencies": { + "@codemirror/lang-html": "6.4.11", + "@dnd-kit/sortable": "7.0.2", + "@ebay/nice-modal-react": "1.2.13", + "@sentry/react": "7.120.4", + "@tanstack/react-query": "4.36.1", + "@tryghost/color-utils": "0.2.16", + "@tryghost/i18n": "workspace:*", + "@tryghost/kg-unsplash-selector": "0.3.26", + "@tryghost/limit-service": "1.5.2", + "@tryghost/nql": "0.12.10", + "@tryghost/timezone-data": "0.4.18", + "@uiw/react-codemirror": "4.25.2", + "clsx": "2.1.1", + "lucide-react": "0.577.0", + "mingo": "2.5.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-hot-toast": "2.6.0", + "react-select": "5.10.2", + "sonner": "2.0.7", + "validator": "13.12.0" + }, + "devDependencies": { + "@playwright/test": "1.59.1", + "@testing-library/jest-dom": "^6", + "@testing-library/react": "14.3.1", + "@tryghost/admin-x-design-system": "workspace:*", + "@tryghost/admin-x-framework": "workspace:*", + "@tryghost/custom-fonts": "1.0.8", + "@tryghost/shade": "workspace:*", + "@types/node": "22.19.17", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@types/validator": "13.15.10", + "@vitejs/plugin-react": "4.7.0", + "eslint": "catalog:", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-tailwindcss": "4.0.0-beta.0", + "stylelint": "15.11.0", + "tailwindcss": "^4.2.2", + "vite": "5.4.21", + "vite-plugin-css-injected-by-js": "3.5.2", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + }, + "nx": { + "targets": { + "dev": { + "dependsOn": [ + "^build" + ] + }, + "test:acceptance": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/admin-x-settings/playwright.config.mjs b/apps/admin-x-settings/playwright.config.mjs new file mode 100644 index 0000000..8fa5955 --- /dev/null +++ b/apps/admin-x-settings/playwright.config.mjs @@ -0,0 +1,3 @@ +import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright'; + +export default adminXPlaywrightConfig(); diff --git a/apps/admin-x-settings/postcss.config.cjs b/apps/admin-x-settings/postcss.config.cjs new file mode 100644 index 0000000..8799f4a --- /dev/null +++ b/apps/admin-x-settings/postcss.config.cjs @@ -0,0 +1 @@ +module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs'); diff --git a/apps/admin-x-settings/src/app.tsx b/apps/admin-x-settings/src/app.tsx new file mode 100644 index 0000000..4907335 --- /dev/null +++ b/apps/admin-x-settings/src/app.tsx @@ -0,0 +1,38 @@ +import MainContent from './main-content'; +import NiceModal from '@ebay/nice-modal-react'; +import SettingsAppProvider, {type UpgradeStatusType} from './components/providers/settings-app-provider'; +import SettingsRouter, {loadModals, modalPaths} from './components/providers/settings-router'; +import {DesignSystemApp, type DesignSystemAppProps} from '@tryghost/admin-x-design-system'; +import {FrameworkProvider, type TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; +import {RoutingProvider} from '@tryghost/admin-x-framework/routing'; + +interface AppProps { + designSystem: DesignSystemAppProps; + upgradeStatus?: UpgradeStatusType; +} + +export function App({designSystem, upgradeStatus}: AppProps) { + return ( + + {/* NOTE: we need to have an extra NiceModal.Provider here because the one inside DesignSystemApp + is loaded too late for possible modals in RoutingProvider, and it's quite hard to change it at + this point */} + + + + + + + + + + ); +} + +export function StandaloneApp({framework, designSystem, upgradeStatus}: AppProps & {framework: TopLevelFrameworkProps}) { + return ( + + + + ); +} diff --git a/apps/admin-x-settings/src/index.tsx b/apps/admin-x-settings/src/index.tsx new file mode 100644 index 0000000..d3c27bb --- /dev/null +++ b/apps/admin-x-settings/src/index.tsx @@ -0,0 +1,6 @@ +import './styles/index.css'; +import {StandaloneApp} from './app.tsx'; + +export { + StandaloneApp as AdminXApp +}; diff --git a/apps/admin-x-settings/src/main-content.tsx b/apps/admin-x-settings/src/main-content.tsx new file mode 100644 index 0000000..7d9cf13 --- /dev/null +++ b/apps/admin-x-settings/src/main-content.tsx @@ -0,0 +1,95 @@ +import ExitSettingsButton from './components/exit-settings-button'; +import Settings from './components/settings'; +import Sidebar from './components/sidebar'; +import Users from './components/settings/general/users'; +import {Heading, confirmIfDirty, topLevelBackdropClasses, useGlobalDirtyState} from '@tryghost/admin-x-design-system'; +import {type ReactNode, useEffect} from 'react'; +import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users'; +import {toast} from 'react-hot-toast'; +import {useGlobalData} from './components/providers/global-data-provider'; +import {useRouting} from '@tryghost/admin-x-framework/routing'; + +const EMPTY_KEYWORDS: string[] = []; + +const Page: React.FC<{children: ReactNode}> = ({children}) => { + return <> +
+ +
+
+ {children} +
+ ; +}; + +const MainContent: React.FC = () => { + const {currentUser} = useGlobalData(); + const {loadingModal} = useRouting(); + const {isDirty} = useGlobalDirtyState(); + + const navigateAway = (escLocation: string) => { + window.location.hash = escLocation; + }; + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + // Don't navigate away if a modal is open - let the modal handle ESC + const modalBackdrop = document.getElementById('modal-backdrop'); + if (modalBackdrop) { + return; + } + + confirmIfDirty(isDirty, () => { + navigateAway('/'); + }); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isDirty]); + + useEffect(() => { + // resets any toasts that may have been left open on initial load + toast.remove(); + }, []); + + // Contributors/Authors only see their profile modal (rendered via routing) + // Don't render the main settings content for them + if (!canAccessSettings(currentUser)) { + return null; + } + + if (isEditorUser(currentUser)) { + return ( + +
+
+ Settings + +
+
+
+ ); + } + + return ( + + {loadingModal &&
} +
+
+ +
+
+
+ +
+ + ); +}; + +export default MainContent; diff --git a/apps/admin-x-settings/src/main.tsx b/apps/admin-x-settings/src/main.tsx new file mode 100644 index 0000000..3631221 --- /dev/null +++ b/apps/admin-x-settings/src/main.tsx @@ -0,0 +1,6 @@ +import './styles/index.css'; +import renderStandaloneApp from '@tryghost/admin-x-framework/test/render'; +import {StandaloneApp} from './app.tsx'; + +renderStandaloneApp(StandaloneApp, { +}); diff --git a/apps/admin-x-settings/src/typings.d.ts b/apps/admin-x-settings/src/typings.d.ts new file mode 100644 index 0000000..d44b415 --- /dev/null +++ b/apps/admin-x-settings/src/typings.d.ts @@ -0,0 +1,10 @@ +declare module '@tryghost/limit-service' +declare module '@tryghost/nql' + +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; + } diff --git a/apps/admin-x-settings/src/vite-env.d.ts b/apps/admin-x-settings/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/admin-x-settings/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/admin-x-settings/test/setup.ts b/apps/admin-x-settings/test/setup.ts new file mode 100644 index 0000000..5313131 --- /dev/null +++ b/apps/admin-x-settings/test/setup.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom/vitest'; +import {setupShadeMocks} from '@tryghost/admin-x-framework/test/setup'; + +// Set up common mocks for shade components +setupShadeMocks(); \ No newline at end of file diff --git a/apps/admin-x-settings/tsconfig.declaration.json b/apps/admin-x-settings/tsconfig.declaration.json new file mode 100644 index 0000000..c7b87e9 --- /dev/null +++ b/apps/admin-x-settings/tsconfig.declaration.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/admin-x-settings/tsconfig.json b/apps/admin-x-settings/tsconfig.json new file mode 100644 index 0000000..e93d93d --- /dev/null +++ b/apps/admin-x-settings/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["node", "vitest/globals", "@testing-library/jest-dom/vitest"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@src/*": ["./src/*"], + "@test/*": ["./test/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "erasableSyntaxOnly": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "test"] +} diff --git a/apps/admin-x-settings/vite.config.mjs b/apps/admin-x-settings/vite.config.mjs new file mode 100644 index 0000000..6e1d8b7 --- /dev/null +++ b/apps/admin-x-settings/vite.config.mjs @@ -0,0 +1,38 @@ +import adminXViteConfig from '@tryghost/admin-x-framework/vite'; +import pkg from './package.json'; +import {resolve} from 'path'; +import {createRequire} from 'node:module'; +const require = createRequire(import.meta.url); + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return adminXViteConfig({ + packageName: pkg.name, + entry: resolve(__dirname, 'src/index.tsx'), + overrides: { + define: { + 'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package + }, + resolve: { + // Shim node modules utilized by the @tryghost/nql package + alias: { + '@src': resolve(__dirname, 'src'), + '@test': resolve(__dirname, 'test'), + fs: 'node-shim.cjs', + path: 'node-shim.cjs', + util: 'node-shim.cjs', + // @TODO: Remove this when @tryghost/nql is updated + mingo: require.resolve('mingo/dist/mingo.js') + } + }, + optimizeDeps: { + include: ['@tryghost/kg-unsplash-selector', '@tryghost/custom-fonts'] + } + }, + build: { + commonjsOptions: { + include: [/ghost\/custom-fonts/] + } + } + }); +}); diff --git a/apps/admin-x-settings/vitest.config.ts b/apps/admin-x-settings/vitest.config.ts new file mode 100644 index 0000000..045e75e --- /dev/null +++ b/apps/admin-x-settings/vitest.config.ts @@ -0,0 +1,3 @@ +import {createVitestConfig} from '@tryghost/admin-x-framework/test/vitest-config'; + +export default createVitestConfig(); \ No newline at end of file diff --git a/apps/admin/.gitignore b/apps/admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 0000000..cbcbc8d --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,27 @@ +# Ghost Admin (React) + +New React-based Ghost admin interface, gradually replacing the existing Ember admin. + +## Architecture + +Uses an **Ember Bridge** system for smooth migration: +- Routes ported to React render React components +- Unported routes fall back to the existing Ember admin +- Both share the same UI space seamlessly + +## Development + +```bash +# Start development server (from monorepo root) +pnpm dev +``` + +## Building for Production + +```bash +# Build production bundle +pnpm nx run @tryghost/admin:build +``` + +This outputs to `apps/admin/dist/` and updates the assets in `ghost/core/core/built/admin/`. + diff --git a/apps/admin/eslint.config.js b/apps/admin/eslint.config.js new file mode 100644 index 0000000..a25e18f --- /dev/null +++ b/apps/admin/eslint.config.js @@ -0,0 +1,117 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tailwindcss from 'eslint-plugin-tailwindcss' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' +import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths' +import ghostPlugin from 'eslint-plugin-ghost'; + +const noHardcodedGhostPaths = { + meta: { + type: 'problem', + docs: { + description: 'Disallow hardcoded /ghost/ paths that break subdirectory installations', + }, + messages: { + noHardcodedPath: 'Do not hardcode /ghost/ paths. Use getGhostPaths() from @tryghost/admin-x-framework/helpers to support subdirectory installations.', + }, + }, + create(context) { + const pattern = /^\/ghost\//; + return { + Literal(node) { + if (typeof node.value === 'string' && pattern.test(node.value)) { + context.report({node, messageId: 'noHardcodedPath'}); + } + }, + TemplateLiteral(node) { + const first = node.quasis[0]; + if (first && pattern.test(first.value.raw)) { + context.report({node, messageId: 'noHardcodedPath'}); + } + }, + }; + }, +}; + +const localPlugin = { + rules: { + 'no-hardcoded-ghost-paths': noHardcodedGhostPaths, + }, +}; +const tailwindCssConfig = `${import.meta.dirname}/src/index.css`; + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommendedTypeChecked, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + plugins: { + 'no-relative-import-paths': noRelativeImportPaths, + ghost: ghostPlugin, + local: localPlugin, + tailwindcss, + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + ecmaVersion: 2020, + globals: globals.browser, + }, + settings: { + tailwindcss: { + config: tailwindCssConfig, + }, + }, + rules: { + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + 'no-restricted-imports': ['error', { + paths: [{ + name: '@tryghost/shade', + message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).', + }], + }], + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/no-contradicting-classname': 'error', + }, + }, + // Apply no-relative-import-paths rule for src files (auto-fix supported) + { + files: ['src/**/*.{ts,tsx}'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { allowSameFolder: true, rootDir: 'src', prefix: '@' }, + ], + }, + }, + // Prevent hardcoded /ghost/ paths in production code (not tests, where mocks need fixed paths) + { + files: ['src/**/*.{ts,tsx}'], + ignores: ['src/**/*.test.*'], + rules: { + 'local/no-hardcoded-ghost-paths': 'error', + }, + }, + // Apply no-relative-import-paths rule for test-utils files + // Note: auto-fix may produce incorrect paths for cross-directory imports + // Use the correct alias manually: @/* for src/, @test-utils/* for test-utils/ + { + files: ['test-utils/**/*.{ts,tsx}'], + rules: { + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { allowSameFolder: true }, + ], + }, + }, +]) diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..fddde02 --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,36 @@ + + + + + + Ghost + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+
+ + + diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..7185681 --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,105 @@ +{ + "name": "@tryghost/admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "pnpm test:unit", + "test:unit": "vitest run", + "typecheck": "tsc -b" + }, + "dependencies": { + "@tryghost/activitypub": "workspace:*", + "@tryghost/admin-x-framework": "workspace:*", + "@tryghost/admin-x-settings": "workspace:*", + "@tryghost/koenig-lexical": "1.7.30", + "@tryghost/posts": "workspace:*", + "@tryghost/shade": "workspace:*", + "@tryghost/stats": "workspace:*", + "mingo": "2.5.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "zod": "4.1.12" + }, + "devDependencies": { + "@eslint/js": "catalog:eslint9", + "@tailwindcss/vite": "4.2.1", + "@tanstack/react-query": "4.36.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "14.3.1", + "@types/node": "25.6.0", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react-swc": "4.1.0", + "eslint": "catalog:eslint9", + "eslint-plugin-no-relative-import-paths": "1.6.1", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-tailwindcss": "4.0.0-beta.0", + "globals": "17.4.0", + "jest-extended": "7.0.0", + "jsdom": "28.1.0", + "msw": "2.12.14", + "sirv": "3.0.2", + "tailwindcss": "^4.2.2", + "typescript": "5.9.3", + "typescript-eslint": "8.58.0", + "vite": "7.1.12", + "vite-tsconfig-paths": "5.1.4", + "vitest": "4.1.2" + }, + "nx": { + "targets": { + "dev": { + "dependsOn": [ + "ghost-admin:dev", + "@tryghost/admin-x-framework:dev", + "@tryghost/admin-x-design-system:dev", + "@tryghost/shade:dev" + ] + }, + "build:dev": { + "dependsOn": [ + "build", + { + "projects": [ + "ghost-admin" + ], + "target": "build:dev" + } + ] + }, + "build": { + "outputs": [ + "{projectRoot}/dist", + "{workspaceRoot}/ghost/core/core/built/admin" + ], + "dependsOn": [ + "build", + { + "projects": [ + "ghost-admin" + ], + "target": "build" + } + ] + }, + "lint": { + "dependsOn": [ + { + "projects": [ + "@tryghost/admin-x-framework", + "@tryghost/admin-x-design-system", + "@tryghost/shade" + ], + "target": "build" + } + ] + } + } + } +} diff --git a/apps/admin/src/app.tsx b/apps/admin/src/app.tsx new file mode 100644 index 0000000..95f5950 --- /dev/null +++ b/apps/admin/src/app.tsx @@ -0,0 +1,29 @@ +import { Outlet } from "@tryghost/admin-x-framework"; +import { useCurrentUser } from "@tryghost/admin-x-framework/api/current-user"; +import { EmberProvider, EmberFallback, EmberRoot } from "./ember-bridge"; +import { AdminLayout } from "./layout/admin-layout"; +import { useEmberAuthSync, useEmberDataSync } from "./ember-bridge"; + +function App() { + const { data: currentUser } = useCurrentUser(); + useEmberAuthSync(); + useEmberDataSync(); + + return ( + + {currentUser ? + + + + + : + <> + + + + } + + ); +} + +export default App; diff --git a/apps/admin/src/index.css b/apps/admin/src/index.css new file mode 100644 index 0000000..b6dc3b8 --- /dev/null +++ b/apps/admin/src/index.css @@ -0,0 +1,178 @@ +@source "../../shade/src/**/*.{ts,tsx}"; +@source "../../posts/src/**/*.{ts,tsx}"; +@source "../../stats/src/**/*.{ts,tsx}"; +@source "../../activitypub/src/**/*.{ts,tsx}"; +@source "../../admin-x-settings/src/**/*.{ts,tsx}"; +@source "../../admin-x-design-system/src/**/*.{ts,tsx}"; +@source "../../../node_modules/@tryghost/kg-unsplash-selector/dist/**/*.js"; + +@import "@tryghost/shade/styles.css"; + +/* Legacy utility compatibility (Spirit/Tachyons-style percentages). */ +.w-100 { + width: 100%; +} + +.h-100 { + height: 100%; +} + +/* Keep admin-x heading line-height consistent with pre-migration settings render. */ +.admin-x-base h1, +.admin-x-base h2, +.admin-x-base h3, +.admin-x-base h4, +.admin-x-base h5 { + line-height: 1.25em; +} + +/* ActivityPub onboarding animations previously defined in admin tailwind config */ +@keyframes lineExpand { + 0% { + transform: scaleX(0); + transform-origin: right; + } + + 100% { + transform: scaleX(1); + transform-origin: right; + } +} + +@keyframes scale { + 0% { + transform: scale(0.8); + } + + 70% { + transform: scale(1.1); + } + + 100% { + transform: scale(1); + } +} + +.animate-onboarding-handle-bg { + opacity: 1; + animation: fadeIn 0.2s ease-in 0.5s forwards; +} + +.animate-onboarding-handle-line { + animation: lineExpand 0.2s ease-in-out 0.7s forwards; +} + +.before\:animate-onboarding-handle-bg::before { + opacity: 1; + animation: fadeIn 0.2s ease-in 0.5s forwards; +} + +.after\:animate-onboarding-handle-line::after { + animation: lineExpand 0.2s ease-in-out 0.7s forwards; + transform: scaleX(1) !important; + transform-origin: right; +} + +.animate-onboarding-handle-label { + opacity: 1; + animation: fadeIn 0.2s ease-in 1.2s forwards; +} + +.animate-onboarding-next-button { + opacity: 1; + animation: fadeIn 0.2s ease-in 2s forwards; +} + +.animate-onboarding-followers { + opacity: 1; + transform: scale(1); + animation: fadeIn 0.2s ease-in 0.5s forwards, scale 0.3s ease-in 0.5s forwards; +} + +.break-anywhere { + overflow-wrap: anywhere; +} + +/* Base layout - grid structure for alerts and main content */ +body.react-admin { + height: 100svh; + display: grid; + grid-template-rows: auto 1fr; + grid-template-columns: 100%; + overflow: hidden; /* Prevent body scroll */ +} + +/* Alerts show at the top and push content down */ +body.react-admin #ember-alerts-wormhole { + grid-row: 1; + grid-column: 1; + z-index: 0; /* Hide alerts when settings app modal is open */ +} + +/* Main app container - pass through to children (.shade.shade-admin) */ +body.react-admin #root { + display: contents; +} + +/* Ensure ShadeApp takes full grid space */ +body.react-admin #root .shade.shade-admin { + grid-row: 2; + grid-column: 1; + min-height: 0; + overflow: hidden; +} + +/* iOS safe area handling for mobile navbar */ +body.react-admin .safe-area-inset-bottom { + padding-bottom: env(safe-area-inset-bottom); +} + +/* Ensure the Ember app renders in the correct position and takes full width/height */ +body.react-admin #ember-app { + width: 100%; + height: 100%; +} + +/* Temporary overrides until Ember transition is finished */ +body.react-admin .gh-canvas-header { + padding-top: 24px; + padding-bottom: 24px; +} + +body.react-admin .gh-canvas-breadcrumb+.gh-canvas-title { + padding-top: 0px; + margin-top: -4px; +} + +body.react-admin .gh-canvas-title, +body.react-admin [data-header="header-title"] { + font-size: 25px; +} + +body.react-admin [data-header="header"] { + padding-top: 24px; + /* padding-bottom: 18px; */ +} + +body.react-admin [data-navbar="navbar"] { + padding-top: 24px; + padding-bottom: 24px; +} + +body.react-admin #ember-app .gh-viewport { + padding-bottom: 0px; +} + +body.react-admin .gh-canvas-header { + z-index: 40; +} + +body.react-admin .members-header { + position: relative; +} + +body.react-admin [data-test-table="members"] thead, +body.react-admin [data-test-table="members"] tr { + position: relative; + z-index: 30; +} diff --git a/apps/admin/src/index.tsx b/apps/admin/src/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx new file mode 100644 index 0000000..33d2c48 --- /dev/null +++ b/apps/admin/src/main.tsx @@ -0,0 +1,52 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./app.tsx"; +import { FrameworkProvider, RouterProvider } from "@tryghost/admin-x-framework"; +import { ShadeApp } from "@tryghost/shade/app"; + +import { routes } from "./routes.tsx"; +import { navigateTo } from "./utils/navigation"; +import { AppProvider } from "./providers/app-provider"; + +const framework = { + ghostVersion: "", + externalNavigate: (link: { route: string; isExternal: boolean }) => { + navigateTo(link.route); + }, + unsplashConfig: { + Authorization: "Client-ID 8672af113b0a8573edae3aa3713886265d9bb741d707f6c01a486cde8c278980", + "Accept-Version": "v1", + "Content-Type": "application/json", + "App-Pragma": "no-cache", + "X-Unsplash-Cache": true, + }, + sentryDSN: null, + onUpdate: (dataType: string, response: unknown) => { + window.EmberBridge?.state.onUpdate(dataType, response); + }, + onInvalidate: (dataType: string) => { + window.EmberBridge?.state.onInvalidate(dataType); + }, + onDelete: (dataType: string, id: string) => { + window.EmberBridge?.state.onDelete(dataType, id); + }, +}; + +createRoot(document.getElementById("root")!).render( + + + + + + + + + + + +); diff --git a/apps/admin/src/members-route.test.tsx b/apps/admin/src/members-route.test.tsx new file mode 100644 index 0000000..a8e90fb --- /dev/null +++ b/apps/admin/src/members-route.test.tsx @@ -0,0 +1,79 @@ +import {render, screen} from '@testing-library/react'; +import React from 'react'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {MembersRoute} from './members-route'; + +const {mockCanManageMembers, mockUseCurrentUser} = vi.hoisted(() => ({ + mockCanManageMembers: vi.fn(), + mockUseCurrentUser: vi.fn() +})); + +vi.mock('@tryghost/admin-x-framework', () => ({ + Outlet: () => React.createElement('div', {'data-testid': 'outlet'}), + Navigate: ({replace, to}: {replace?: boolean; to: string}) => React.createElement('div', { + 'data-replace': String(Boolean(replace)), + 'data-testid': 'navigate', + 'data-to': to + }) +})); + +vi.mock('@tryghost/admin-x-framework/api/current-user', () => ({ + useCurrentUser: mockUseCurrentUser +})); + +vi.mock('@tryghost/admin-x-framework/api/users', () => ({ + canManageMembers: mockCanManageMembers +})); + +describe('MembersRoute', () => { + beforeEach(() => { + mockCanManageMembers.mockReturnValue(true); + mockUseCurrentUser.mockReturnValue({ + data: { + id: '1', + roles: [{name: 'Administrator'}] + }, + isError: false, + isLoading: false + }); + }); + + it('renders the nested members routes for authorized users', () => { + render(); + + expect(screen.getByTestId('outlet')).toBeInTheDocument(); + }); + + it('redirects users without member permissions to home', () => { + mockCanManageMembers.mockReturnValue(false); + + render(); + + expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/'); + expect(screen.getByTestId('navigate')).toHaveAttribute('data-replace', 'true'); + }); + + it('renders nothing while the current user is still loading', () => { + mockUseCurrentUser.mockReturnValue({ + data: undefined, + isError: false, + isLoading: true + }); + + const {container} = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('redirects to home when the current user is unavailable after loading', () => { + mockUseCurrentUser.mockReturnValue({ + data: undefined, + isError: false, + isLoading: false + }); + + render(); + + expect(screen.getByTestId('navigate')).toHaveAttribute('data-to', '/'); + }); +}); diff --git a/apps/admin/src/members-route.tsx b/apps/admin/src/members-route.tsx new file mode 100644 index 0000000..ebff1ed --- /dev/null +++ b/apps/admin/src/members-route.tsx @@ -0,0 +1,21 @@ +import {Navigate, Outlet} from "@tryghost/admin-x-framework"; +import {useCurrentUser} from "@tryghost/admin-x-framework/api/current-user"; +import {canManageMembers} from "@tryghost/admin-x-framework/api/users"; + +export function MembersRoute() { + const {data: currentUser, isError, isLoading} = useCurrentUser(); + + if (!currentUser) { + if (isError || !isLoading) { + return ; + } + + return null; + } + + if (!canManageMembers(currentUser)) { + return ; + } + + return ; +} diff --git a/apps/admin/src/my-profile-redirect.tsx b/apps/admin/src/my-profile-redirect.tsx new file mode 100644 index 0000000..c4754c3 --- /dev/null +++ b/apps/admin/src/my-profile-redirect.tsx @@ -0,0 +1,18 @@ +import {Navigate} from "@tryghost/admin-x-framework"; +import {useCurrentUser} from "@tryghost/admin-x-framework/api/current-user"; + +const MyProfileRedirect = () => { + const {data: currentUser, isError, isLoading} = useCurrentUser(); + + if (!currentUser) { + if (isError || !isLoading) { + return ; + } + + return null; + } + + return ; +}; + +export default MyProfileRedirect; diff --git a/apps/admin/src/not-found.tsx b/apps/admin/src/not-found.tsx new file mode 100644 index 0000000..3b67d6d --- /dev/null +++ b/apps/admin/src/not-found.tsx @@ -0,0 +1,11 @@ +export function NotFound() { + return ( +
+
+

404

+ +

Page not found

+
+
+ ); +} diff --git a/apps/admin/src/routes.tsx b/apps/admin/src/routes.tsx new file mode 100644 index 0000000..c3dd3ef --- /dev/null +++ b/apps/admin/src/routes.tsx @@ -0,0 +1,139 @@ +import {type RouteObject, Outlet, lazyComponent, redirect} from "@tryghost/admin-x-framework"; + +// ActivityPub +import { FeatureFlagsProvider, routes as activityPubRoutes } from "@tryghost/activitypub/api"; + +// Posts (aka tags and post analytics) +import { PostsAppContextProvider, routes as postRoutes } from "@tryghost/posts/api"; + +// Stats (aka analytics) +import { GlobalDataProvider, routes as statsRoutes } from "@tryghost/stats/api"; +import MyProfileRedirect from "./my-profile-redirect"; + +// Ember +import { EmberFallback, ForceUpgradeGuard } from "./ember-bridge"; +import type { RouteHandle } from "./ember-bridge"; +import { MembersRoute } from "./members-route"; + +import { NotFound } from "./not-found"; + +// Routes handled by the Ember admin app. React delegates these to Ember via +// EmberFallback. When migrating a route to React, remove its entry from here. +const EMBER_ROUTES: string[] = [ + "/", + "/dashboard", + "/site", + "/launch", + "/setup/*", + "/signin/*", + "/signout", + "/signup/*", + "/reset/*", + "/pro/*", + "/posts", + "/posts/analytics/:postId/mentions", + "/posts/analytics/:postId/debug", + "/restore", + "/pages", + "/editor/*", + "/tags/new", + "/explore/*", + "/migrate/*", + "/members/new", + "/members/:member_id", + "/members-activity", + "/designsandbox", + "/mentions", +]; + +const emberFallbackHandle = { allowInForceUpgrade: true } satisfies RouteHandle; + +const emberFallbackRoutes: RouteObject[] = EMBER_ROUTES.map(path => ({ + path, + Component: EmberFallback, + handle: emberFallbackHandle, +})); + +const membersRoute: RouteObject = { + path: "/members", + element: , + handle: emberFallbackHandle, + children: [ + { + index: true, + lazy: lazyComponent(() => import("@tryghost/posts/members")) + }, + { + path: "import", + lazy: lazyComponent(() => import("@tryghost/posts/members")) + } + ] +}; + +export const routes: RouteObject[] = [ + { + // ForceUpgradeGuard wraps all routes to redirect to /pro when in force upgrade mode. + // Routes with handle.allowInForceUpgrade: true bypass this protection. + element: , + children: [ + { + // Override the tag detail route from the posts app to ensure we + // correctly delegate to Ember since we can't remove the blank screen in + // the posts app. The blank screen needs to be there to prevent the + // router error fallback from triggering when navigating from the tag + // list to a tag detail page. + path: "/tags/:tagSlug", + Component: EmberFallback, + handle: emberFallbackHandle, + }, + membersRoute, + { + element: ( + + + + ), + // Filter out catch-all routes + children: postRoutes[0].children!.filter((route) => route.path !== "*"), + }, + { + element: ( + + + + ), + children: statsRoutes, + }, + { + path: `network`, + loader: () => redirect("/activitypub"), + }, + { + path: "my-profile", + Component: MyProfileRedirect, + handle: { allowInForceUpgrade: true } satisfies RouteHandle, + }, + { + path: "", + element: ( + + + + ), + children: activityPubRoutes, + }, + { + path: `settings/*`, + lazy: lazyComponent(() => import("./settings/settings")), + handle: { allowInForceUpgrade: true } satisfies RouteHandle, + }, + // Ember-handled routes + ...emberFallbackRoutes, + { + // 404 catch-all for routes not handled by React or Ember + path: "*", + Component: NotFound, + }, + ], + }, +]; diff --git a/apps/admin/src/vite-env.d.ts b/apps/admin/src/vite-env.d.ts new file mode 100644 index 0000000..2c57933 --- /dev/null +++ b/apps/admin/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +declare module '@tryghost/limit-service' +declare module '@tryghost/nql' +declare module '@tryghost/koenig-lexical' diff --git a/apps/admin/test-utils/setup.ts b/apps/admin/test-utils/setup.ts new file mode 100644 index 0000000..8460b4a --- /dev/null +++ b/apps/admin/test-utils/setup.ts @@ -0,0 +1,8 @@ +import "@testing-library/jest-dom"; +import { expect } from "vitest"; +import matchers from "jest-extended"; +import { setupShadeMocks } from "@tryghost/admin-x-framework/test/setup"; + +expect.extend(matchers); + +setupShadeMocks(); diff --git a/apps/admin/test-utils/test-helpers.ts b/apps/admin/test-utils/test-helpers.ts new file mode 100644 index 0000000..7f2b779 --- /dev/null +++ b/apps/admin/test-utils/test-helpers.ts @@ -0,0 +1,13 @@ +import { waitFor } from "@testing-library/react"; +import { expect } from "vitest"; +import type { UseQueryResult } from "@tanstack/react-query"; + +export async function waitForQuerySettled(result: { current: UseQueryResult }) { + await waitFor( + () => { + // Query is settled when it has reached a terminal state (success or error) + const isSettled = (result.current.isSuccess || result.current.isError) && !result.current.isFetching; + expect(isSettled).toBe(true); + } + ); +} diff --git a/apps/admin/tsconfig.app.json b/apps/admin/tsconfig.app.json new file mode 100644 index 0000000..271cc1e --- /dev/null +++ b/apps/admin/tsconfig.app.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["node", "vitest/globals"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@test-utils/*": ["./test-utils/*"], + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "test-utils"], + "references": [ + { "path": "../admin-x-framework/tsconfig.declaration.json" }, + { "path": "../posts/tsconfig.declaration.json" }, + { "path": "../stats/tsconfig.declaration.json" }, + { "path": "../activitypub/tsconfig.declaration.json" } + ] +} diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/admin/tsconfig.node.json b/apps/admin/tsconfig.node.json new file mode 100644 index 0000000..693dde0 --- /dev/null +++ b/apps/admin/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["node"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["./vite*.ts",] +} diff --git a/apps/admin/vite-backend-proxy.ts b/apps/admin/vite-backend-proxy.ts new file mode 100644 index 0000000..441aeff --- /dev/null +++ b/apps/admin/vite-backend-proxy.ts @@ -0,0 +1,141 @@ +import type { Plugin, ProxyOptions } from "vite"; +import type { IncomingMessage } from "http"; +import { getSubdir, GHOST_URL } from "./vite.config"; + +/** + * Resolves the configured Ghost site URL by calling the admin api site endpoint + * with retries (up to 20 seconds). + */ +async function resolveGhostSiteUrl() { + const MAX_ATTEMPTS = 20; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + const siteEndpoint = new URL('ghost/api/admin/site/', GHOST_URL); + const response = await fetch(siteEndpoint); + const data = (await response.json()) as { site: { url: string } }; + return { + url: data.site.url, + host: new URL(data.site.url).host, + }; + } catch (error) { + if (attempt === MAX_ATTEMPTS) throw error; + await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); + } + } + + throw new Error("Failed to resolve Ghost site URL"); +} + +/** + * Creates proxy configuration for Ghost Admin API requests. Rewrites cookies + * and headers to work with Ghost's security middleware. + */ +function createAdminApiProxy(site: { + url: string; + host: string; +}): Record { + // When running the dev server against the backend on HTTPS, we need to + // remove the same site and secure flags from the cookie. Otherwise, the + // browser won't set it correctly since the dev server is running on HTTP. + const rewriteCookies = (proxyRes: IncomingMessage) => { + const cookies = proxyRes.headers["set-cookie"]; + if (Array.isArray(cookies)) { + proxyRes.headers["set-cookie"] = cookies.map((cookie) => { + return cookie + .split(";") + .filter((v) => v.trim().toLowerCase() !== "secure") + .filter((v) => v.trim().toLowerCase() !== "samesite=none") + .join("; "); + }); + } + }; + + const subdir = getSubdir(); + + return { + [`^${subdir}/ghost/api/.*`]: { + target: site.url, + changeOrigin: true, + followRedirects: true, + autoRewrite: true, + cookieDomainRewrite: { + "*": site.host, + }, + configure(proxy) { + proxy.on("proxyRes", rewriteCookies); + }, + }, + }; +} + +/** + * Creates proxy configuration for Ember CLI live reload script. + */ +function createEmberLiveReloadProxy(): Record { + return { + "^/ember-cli-live-reload.js": { + target: "http://localhost:4200", + changeOrigin: true, + }, + }; +} + +/** + * Vite plugin that injects proxy configurations for: + * 1. Ghost Admin API - proxies /ghost/api requests to the Ghost backend + * 2. Ember Live Reload - proxies ember-cli-live-reload.js to Ember dev server + */ +export function ghostBackendProxyPlugin(): Plugin { + let siteUrl!: { url: string; host: string }; + + return { + name: "ghost-backend-proxy", + + async configResolved(config) { + // Only resolve backend URL for dev/preview, not for builds or tests + if (config.command !== 'serve' || config.mode === 'test') return; + + try { + // We expect this to succeed immediately, but if the backend + // server is getting started, it might need some time. + // In that case, this lets the user know in case we're barking + // up the wrong tree (aka the GHOST_URL is wrong.) + const timeout = setTimeout(() => { + config.logger.info(`Trying to reach Ghost Admin API at ${GHOST_URL}...`); + }, 1000); + + siteUrl = await resolveGhostSiteUrl(); + clearTimeout(timeout); + + config.logger.info(`👻 Using backend url: ${siteUrl.url}`); + } catch (error) { + config.logger + .error(`Could not reach Ghost Admin API at: ${GHOST_URL} + +Ensure the Ghost backend is running. If needed, set the GHOST_URL environment variable to the correct URL. + `); + + throw error; + } + }, + + configureServer(server) { + if (!siteUrl) return; + + server.config.server.proxy = { + ...server.config.server.proxy, + ...createAdminApiProxy(siteUrl), + ...createEmberLiveReloadProxy(), + }; + }, + + configurePreviewServer(server) { + if (!siteUrl) return; + + server.config.preview.proxy = { + ...server.config.preview.proxy, + ...createAdminApiProxy(siteUrl), + }; + }, + } as const satisfies Plugin; +} diff --git a/apps/admin/vite-deep-links.ts b/apps/admin/vite-deep-links.ts new file mode 100644 index 0000000..6f5477d --- /dev/null +++ b/apps/admin/vite-deep-links.ts @@ -0,0 +1,37 @@ +import type { Plugin, ViteDevServer, PreviewServer } from "vite"; + +/** + * Vite plugin that redirects admin deep-link URLs to hash-based URLs. + * + * Mirrors ghost/core/core/server/web/admin/middleware/redirect-admin-urls.js + * so that direct navigation to paths like /ghost/posts/123 redirects to /ghost/#/posts/123 + * + * By registering as a post-middleware, static assets and API requests are handled first, + * and only unhandled requests trigger the redirect. + */ +export function deepLinksPlugin(): Plugin { + function addRedirectMiddleware(server: ViteDevServer | PreviewServer) { + const base = (server.config.base ?? "/ghost").replace(/\/$/, ""); + const pathRegex = new RegExp(`^${base}/(.+)`); + + return () => { + server.middlewares.use((req, res, next) => { + const match = req.originalUrl?.match(pathRegex); + + if (match) { + res.writeHead(302, { Location: `${base}/#/${match[1]}` }); + res.end(); + return; + } + + next(); + }); + }; + } + + return { + name: "deep-links", + configureServer: addRedirectMiddleware, + configurePreviewServer: addRedirectMiddleware, + }; +} diff --git a/apps/admin/vite-ember-assets.ts b/apps/admin/vite-ember-assets.ts new file mode 100644 index 0000000..0be1f07 --- /dev/null +++ b/apps/admin/vite-ember-assets.ts @@ -0,0 +1,145 @@ +import type {PluginOption, HtmlTagDescriptor, ResolvedConfig} from 'vite'; +import path from 'path'; +import fs from 'fs'; +import sirv from 'sirv'; + +const GHOST_ADMIN_PATH = path.resolve(__dirname, '../../ghost/core/core/built/admin'); +const GHOST_ADMIN_DIST = path.resolve(__dirname, '../../ghost/admin/dist'); + +function isAbsoluteUrl(url: string): boolean { + return url.startsWith('http://') || + url.startsWith('https://') || + url.startsWith('/'); +} + +function prefixUrl(url: string, base: string): string { + if (isAbsoluteUrl(url)) return url; + const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base; + return `${normalizedBase}/${url}`; +} + +// Vite plugin to extract styles and scripts from Ghost admin index.html +export function emberAssetsPlugin() { + let config: ResolvedConfig; + + return { + name: 'ember-assets', + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + transformIndexHtml: { + order: 'post', + handler() { + // Read from Ember's own build output (not the combined output + // in built/admin which gets overwritten by closeBundle and would + // accumulate duplicate path prefixes on repeated builds) + const indexPath = path.resolve(GHOST_ADMIN_DIST, 'index.html'); + try { + const indexContent = fs.readFileSync(indexPath, 'utf-8'); + const base = config.base || '/'; + + // Extract stylesheets + const styleRegex = /]*rel="stylesheet"[^>]*href="([^"]*)"[^>]*>/g; + const styles: HtmlTagDescriptor[] = []; + let styleMatch; + while ((styleMatch = styleRegex.exec(indexContent)) !== null) { + styles.push({ + tag: 'link', + attrs: { + rel: 'stylesheet', + href: prefixUrl(styleMatch[1], base) + } + }); + } + // Extract scripts + const scriptRegex = /]*src="([^"]*)"[^>]*><\/script>/g; + const scripts: HtmlTagDescriptor[] = []; + let scriptMatch; + while ((scriptMatch = scriptRegex.exec(indexContent)) !== null) { + scripts.push({ + tag: 'script', + injectTo: 'body', + attrs: { + src: prefixUrl(scriptMatch[1], base) + } + }); + } + + // Extract meta tags + const metaRegex = /]*>/g; + const metaTags: HtmlTagDescriptor[] = []; + let metaMatch; + while ((metaMatch = metaRegex.exec(indexContent)) !== null) { + metaTags.push({ + tag: 'meta', + attrs: { + name: 'ghost-admin/config/environment', + content: metaMatch[1] + } + }); + } + + // Generate the virtual module content + return [...styles, ...scripts, ...metaTags]; + } catch (error) { + console.warn('Failed to read Ghost admin index.html:', error); + return; + } + } + }, + configureServer(server) { + // Serve Ember assets from the filesystem in development + const assetsMiddleware = sirv(path.resolve(GHOST_ADMIN_PATH, 'assets'), { + dev: true, + etag: true + }); + + const base = (server.config.base ?? '/ghost').replace(/\/$/, ''); + const assetsPrefix = `${base}/assets/`; + + server.middlewares.use((req, res, next) => { + if (req.url?.startsWith(assetsPrefix)) { + const originalUrl = req.url; + req.url = req.url.replace(assetsPrefix, '/'); + assetsMiddleware(req, res, () => { + req.url = originalUrl; + next(); + }); + } else { + next(); + } + }); + }, + closeBundle() { + // Only copy assets during production builds + if (config.command === 'build') { + try { + // All legacy admin assets gets copied to the Ghost core + // admin assets folder by the Ember build + const ghostAssetsDir = path.resolve(GHOST_ADMIN_PATH, 'assets'); + + // React admin build output (apps/admin/dist/) + const reactAssetsDir = path.resolve(config.build.outDir, 'assets'); + const reactIndexFile = path.resolve(config.build.outDir, 'index.html'); + + // Copy Ember assets to React build output to enable use of + // vite preview. This also prevents stale Ember assets from + // overwriting fresh ones in the next step. + fs.cpSync(ghostAssetsDir, reactAssetsDir, { recursive: true }); + + // Copy combined assets back to Ghost core admin assets folder + fs.cpSync(reactAssetsDir, ghostAssetsDir, { + recursive: true, + force: true + }); + + // Copy React index.html, overwriting the existing index.html + const forwardIndexFile = path.resolve(GHOST_ADMIN_PATH, 'index.html'); + fs.copyFileSync(reactIndexFile, forwardIndexFile); + } catch (error) { + throw new Error(`Failed to copy admin assets: ${error instanceof Error ? error.message : String(error)}`); + } + } + } + } as const satisfies PluginOption; +} diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..6b7d056 --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,72 @@ +import { resolve } from "path"; +import { createRequire } from "node:module"; +import { defineConfig } from "vitest/config"; +import type { PluginOption } from "vite"; +const require = createRequire(import.meta.url); +import tsconfigPaths from "vite-tsconfig-paths"; +import react from "@vitejs/plugin-react-swc"; +import tailwindcss from "@tailwindcss/vite"; + +import { emberAssetsPlugin } from "./vite-ember-assets"; +import { ghostBackendProxyPlugin } from "./vite-backend-proxy"; +import { deepLinksPlugin } from "./vite-deep-links"; + +export const GHOST_URL = process.env.GHOST_URL ?? "http://localhost:2368/"; +const GHOST_CARDS_PATH = resolve(__dirname, "../../ghost/core/core/frontend/src/cards"); + +/** + * Extracts the subdirectory path from GHOST_URL. + * e.g., "http://localhost:2368/blog/" -> "/blog" + * "http://localhost:2368/" -> "" + */ +export function getSubdir(): string { + const url = new URL(GHOST_URL); + return url.pathname.replace(/\/$/, ''); +} + +/** + * Computes the Vite base path. + * - If GHOST_CDN_URL is set, use it (for CDN deployments) + * - Otherwise, use the subdir + /ghost (e.g., "/ghost" or "/blog/ghost") + * - For builds without CDN, use "./" for relative paths in index-forward.html + */ +function getBase(command: 'build' | 'serve'): string { + if (process.env.GHOST_CDN_URL) { + return process.env.GHOST_CDN_URL; + } + // During build, use relative paths so index-forward.html works when served from any subdir + if (command === 'build') { + return './'; + } + // During dev, use absolute path based on GHOST_URL subdir + return `${getSubdir()}/ghost`; +} + +// https://vite.dev/config/ +export default defineConfig(({ command }) => ({ + base: getBase(command), + plugins: [tailwindcss() as PluginOption, react(), emberAssetsPlugin(), ghostBackendProxyPlugin(), deepLinksPlugin(), tsconfigPaths()], + define: { + "process.env.DEBUG": false, // Shim env var utilized by the @tryghost/nql package + }, + server: { + host: '0.0.0.0', + port: 5174, + allowedHosts: true + }, + resolve: { + alias: { + "@ghost-cards": GHOST_CARDS_PATH, + // TODO: Remove this when @tryghost/nql is updated + mingo: require.resolve("mingo/dist/mingo.js"), + }, + // Shim node modules utilized by the @tryghost/nql package + external: ["fs", "path", "util"], + }, + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./test-utils/setup.ts"], + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + }, +})); diff --git a/apps/announcement-bar/LICENSE b/apps/announcement-bar/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/apps/announcement-bar/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/announcement-bar/README.md b/apps/announcement-bar/README.md new file mode 100644 index 0000000..ad56d58 --- /dev/null +++ b/apps/announcement-bar/README.md @@ -0,0 +1,39 @@ +# Announcement Bar + +## Development + +### Pre-requisites + +- Run `pnpm` in Ghost monorepo root +- Run `pnpm` in this directory + +### Running via Ghost `pnpm dev` in root folder + +Announcement Bar runs automatically when using Ghost's development command from the monorepo root: +```bash +pnpm dev +``` + +This starts all frontend apps (including Announcement Bar.) + +## Release + +A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released. +In either case, you need sufficient permissions to release `@tryghost` packages on NPM. + +### Patch release + +1. Run `pnpm ship` and select a patch version when prompted +2. Merge the release commit to `main` + +### Minor / major release + +1. Run `pnpm ship` and select a minor or major version when prompted +2. Merge the release commit to `main` +3. Wait until a new version of Ghost is released + +To use the new version of signup form in Ghost, update the version in Ghost core's default configuration (currently at `core/shared/config/default.json`) + +# Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/apps/announcement-bar/package.json b/apps/announcement-bar/package.json new file mode 100644 index 0000000..8b8c168 --- /dev/null +++ b/apps/announcement-bar/package.json @@ -0,0 +1,91 @@ +{ + "name": "@tryghost/announcement-bar", + "version": "1.1.17", + "license": "MIT", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "files": [ + "umd/", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "@tryghost/content-api": "1.12.6", + "react": "17.0.2", + "react-dom": "17.0.2" + }, + "scripts": { + "dev": "concurrently \"vite preview -l silent\" \"pnpm build:watch\"", + "build": "vite build", + "build:watch": "vite build --watch", + "test": "vitest run", + "test:ci": "pnpm test --coverage", + "test:unit": "pnpm test:ci", + "lint": "eslint src --ext .js --cache", + "preship": "pnpm lint", + "ship": "node ../../.github/scripts/release-apps.js", + "prepublishOnly": "pnpm build" + }, + "eslintConfig": { + "env": { + "browser": true, + "jest": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2022 + }, + "extends": [ + "plugin:ghost/browser", + "plugin:react/recommended" + ], + "plugins": [ + "ghost" + ], + "rules": { + "react/prop-types": "off", + "ghost/filenames/match-regex": [ + "error", + "^[a-z0-9.-]+$", + false + ] + }, + "settings": { + "react": { + "version": "detect" + } + } + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "jest": { + "coverageReporters": [ + "cobertura", + "text-summary", + "html" + ] + }, + "devDependencies": { + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "~3.2.4", + "cross-fetch": "4.1.0", + "jsdom": "28.1.0", + "vite": "5.4.21", + "vite-plugin-svgr": "3.3.0", + "vitest": "3.2.4" + } +} diff --git a/apps/announcement-bar/src/app.js b/apps/announcement-bar/src/app.js new file mode 100644 index 0000000..101cdfb --- /dev/null +++ b/apps/announcement-bar/src/app.js @@ -0,0 +1,12 @@ +import React from 'react'; +import {Preview} from './components/preview'; +import {Main} from './components/main'; + +export function App({apiUrl, previewData}) { + if (previewData) { + return ; + } + return ( +
+ ); +} diff --git a/apps/announcement-bar/src/index.js b/apps/announcement-bar/src/index.js new file mode 100644 index 0000000..e1b2b16 --- /dev/null +++ b/apps/announcement-bar/src/index.js @@ -0,0 +1,59 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import {App} from './app'; + +const ROOT_DIV_ID = 'announcement-bar-root'; + +function addRootDiv() { + if (document.getElementById(ROOT_DIV_ID)) { + return; + } + + const elem = document.createElement('div'); + elem.id = ROOT_DIV_ID; + document.body.prepend(elem); +} + +function getSiteData() { + /** + * @type {HTMLElement} + */ + const scriptTag = document.querySelector('script[data-announcement-bar]'); + if (scriptTag) { + const apiUrl = scriptTag.dataset.apiUrl; + return {apiUrl, previewData: getPreviewData(scriptTag)}; + } + return {}; +} + +function getPreviewData(scriptTag) { + if (scriptTag.dataset.preview) { + const announcement = scriptTag.dataset.announcement; + const announcementBackground = scriptTag.dataset.announcementBackground; + + return {announcement, announcement_background: announcementBackground}; + } + + return null; +} + +function setup() { + addRootDiv(); +} + +function init() { + const {apiUrl, previewData} = getSiteData(); + setup(); + ReactDOM.render( + + + , + document.getElementById(ROOT_DIV_ID) + ); +} + +init(); diff --git a/apps/announcement-bar/test/setup-tests.js b/apps/announcement-bar/test/setup-tests.js new file mode 100644 index 0000000..a679905 --- /dev/null +++ b/apps/announcement-bar/test/setup-tests.js @@ -0,0 +1,8 @@ +import {fetch} from 'cross-fetch'; + +// TODO: remove this once we're switched `jest` to `vi` in code +// eslint-disable-next-line no-undef +globalThis.jest = vi; + +// eslint-disable-next-line no-undef +globalThis.fetch = fetch; diff --git a/apps/announcement-bar/vite.config.mjs b/apps/announcement-bar/vite.config.mjs new file mode 100644 index 0000000..2c4893e --- /dev/null +++ b/apps/announcement-bar/vite.config.mjs @@ -0,0 +1,70 @@ +/* eslint-env node */ +import {resolve} from 'path'; +import fs from 'fs/promises'; + +import {defineConfig} from 'vitest/config'; +import reactPlugin from '@vitejs/plugin-react'; +import svgrPlugin from 'vite-plugin-svgr'; + +import pkg from './package.json'; + +export default defineConfig((config) => { + const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name; + + return { + logLevel: process.env.CI ? 'info' : 'warn', + clearScreen: false, + define: { + 'process.env.NODE_ENV': JSON.stringify(config.mode) + }, + preview: { + host: '0.0.0.0', + allowedHosts: true, // allows domain-name proxies to the preview server + port: 4177 + }, + plugins: [ + reactPlugin(), + svgrPlugin() + ], + esbuild: { + loader: 'jsx', + include: /src\/.*\.jsx?$/, + exclude: [] + }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'load-js-files-as-jsx', + setup(build) { + build.onLoad({filter: /(src|test)\/.*\.js$/}, async args => ({ + loader: 'jsx', + contents: await fs.readFile(args.path, 'utf8') + })); + } + } + ] + } + }, + build: { + outDir: resolve(__dirname, 'umd'), + emptyOutDir: true, + reportCompressedSize: false, + minify: true, + sourcemap: true, + cssCodeSplit: true, + lib: { + entry: resolve(__dirname, 'src/index.js'), + formats: ['umd'], + name: pkg.name, + fileName: format => `${outputFileName}.min.js` + } + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './test/setup-tests.js', + testTimeout: 10000 + } + }; +}); diff --git a/apps/comments-ui/.eslintrc.js b/apps/comments-ui/.eslintrc.js new file mode 100644 index 0000000..0180217 --- /dev/null +++ b/apps/comments-ui/.eslintrc.js @@ -0,0 +1,59 @@ +/* eslint-env node */ +const tailwindConfig = `${__dirname}/tailwind.config.cjs`; + +module.exports = { + root: true, + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:i18next/recommended' + ], + plugins: [ + 'ghost', + 'tailwindcss', + 'i18next' + ], + settings: { + react: { + version: 'detect' + } + }, + rules: { + // Sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // TODO: fix + remove this + '@typescript-eslint/no-explicit-any': 'warn', + + // Suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // Ignore prop-types for now + 'react/prop-types': 'off', + + // custom react rules + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + + 'tailwindcss/classnames-order': ['error', {config: tailwindConfig}], + 'tailwindcss/enforces-negative-arbitrary-values': ['warn', {config: tailwindConfig}], + 'tailwindcss/enforces-shorthand': ['warn', {config: tailwindConfig}], + 'tailwindcss/migration-from-tailwind-2': ['warn', {config: tailwindConfig}], + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': ['error', {config: tailwindConfig}], + + // This rule doesn't work correctly with TypeScript, and TypeScript has its own better version + 'no-undef': 'off' + } +}; diff --git a/apps/comments-ui/LICENSE b/apps/comments-ui/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/apps/comments-ui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/comments-ui/README.md b/apps/comments-ui/README.md new file mode 100644 index 0000000..2df8569 --- /dev/null +++ b/apps/comments-ui/README.md @@ -0,0 +1,39 @@ +# Comments UI + +Comments widget that is embedded at the bottom of posts in Ghost. + +## Development + +### Pre-requisites + +- Run `pnpm` in Ghost monorepo root + +### Running via Ghost `pnpm dev` in root folder + +Comments UI runs automatically when using Ghost's development command from the monorepo root: +```bash +pnpm dev +``` + +## Release + +A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released. In either case, you need sufficient permissions to release `@tryghost` packages on NPM. + +### Patch release + +1. Run `pnpm ship` and select a patch version when prompted +2. Merge the release commit to `main` + +### Minor / major release + +1. Run `pnpm ship` and select a minor or major version when prompted +2. Merge the release commit to `main` +3. Wait until a new version of Ghost is released + +### JsDelivr cache +If the CI doesn't clear JsDelivr cache to get the new version out instantly, you may want to do it yourself manually ([docs](https://www.notion.so/ghost/How-to-clear-jsDelivr-CDN-cache-2930bdbac02946eca07ac23ab3199bfa?pvs=4)). Typically, you'll need to open `https://purge.jsdelivr.net/ghost/comments-ui@~${COMMENTS_UI_VERSION}/umd/comments-ui.min.js` and +`https://purge.jsdelivr.net/ghost/comments-ui@~${COMMENTS_UI_VERSION}/umd/main.css` in your browser, where `COMMENTS_UI_VERSION` is the latest minor version in `ghost/core/core/shared/config/defaults.json` ([code](https://github.com/TryGhost/Ghost/blob/0aef3d3beeebcd79a4bfd3ad27e0ac67554b5744/ghost/core/core/shared/config/defaults.json#L198)) + +# Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json new file mode 100644 index 0000000..77d2445 --- /dev/null +++ b/apps/comments-ui/package.json @@ -0,0 +1,89 @@ +{ + "name": "@tryghost/comments-ui", + "version": "1.4.6", + "license": "MIT", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "unpkg": "umd/comments-ui.umd.js", + "files": [ + "umd/", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "dev": "concurrently \"pnpm preview --host -l silent\" \"pnpm build:watch\"", + "dev:test": "vite build && vite preview --port 7175", + "build": "vite build", + "build:watch": "vite build --watch", + "preview": "vite preview", + "test": "pnpm test:unit && pnpm test:e2e", + "test:unit": "vitest run --coverage", + "test:e2e": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", + "test:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=1000 pnpm test:e2e --headed", + "test:e2e:full": "ALL_BROWSERS=1 pnpm test:e2e", + "lint": "eslint src --ext .js,.ts,.jsx,.tsx --cache", + "preship": "pnpm lint", + "ship": "node ../../.github/scripts/release-apps.js", + "prepublishOnly": "pnpm build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@doist/react-interpolate": "2.2.1", + "@headlessui/react": "1.7.19", + "@tiptap/core": "2.26.3", + "@tiptap/extension-blockquote": "2.26.3", + "@tiptap/extension-document": "2.26.3", + "@tiptap/extension-hard-break": "2.26.3", + "@tiptap/extension-link": "2.26.3", + "@tiptap/extension-paragraph": "2.26.3", + "@tiptap/extension-placeholder": "2.26.3", + "@tiptap/extension-text": "2.26.3", + "@tiptap/pm": "2.26.3", + "@tiptap/react": "2.26.3", + "@tryghost/debug": "0.1.40", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-string-replace": "1.1.1" + }, + "devDependencies": { + "@playwright/test": "1.59.1", + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "12.1.5", + "@testing-library/user-event": "14.6.1", + "@tryghost/i18n": "workspace:*", + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "0.34.6", + "autoprefixer": "10.4.21", + "bson-objectid": "2.0.4", + "concurrently": "8.2.2", + "eslint": "catalog:", + "eslint-plugin-i18next": "6.1.3", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-tailwindcss": "3.18.2", + "jsdom": "28.1.0", + "moment": "2.30.1", + "postcss": "8.5.6", + "sinon": "^21.1.1", + "tailwindcss": "3.4.18", + "vite": "5.4.21", + "vite-plugin-css-injected-by-js": "3.5.2", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + } +} diff --git a/apps/comments-ui/playwright.config.ts b/apps/comments-ui/playwright.config.ts new file mode 100644 index 0000000..730706d --- /dev/null +++ b/apps/comments-ui/playwright.config.ts @@ -0,0 +1,64 @@ +import {defineConfig, devices} from '@playwright/test'; + +export const E2E_PORT = 7175; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './test/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Hardcode to use all cores in CI */ + workers: process.env.CI ? '100%' : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.PLAYWRIGHT_REPORTER ?? 'html', + timeout: process.env.PLAYWRIGHT_SLOWMO ? 100000 : 20000, + expect: { + timeout: process.env.PLAYWRIGHT_SLOWMO ? 100000 : 5000 + }, + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + launchOptions: { + slowMo: parseInt(process.env.PLAYWRIGHT_SLOWMO ?? '') || 0, + // force GPU hardware acceleration + // (even in headless mode) + args: ['--use-gl=egl'] + }, + permissions: ['local-network-access'] + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']} + }, + + ...(process.env.ALL_BROWSERS ? [{ + name: 'firefox', + use: {...devices['Desktop Firefox']} + }, + + { + name: 'webkit', + use: {...devices['Desktop Safari']} + }] : []) + ], + + /* Run local dev server before starting the tests */ + webServer: { + command: `pnpm dev:test`, + url: `http://localhost:${E2E_PORT}/comments-ui.min.js`, + reuseExistingServer: !process.env.CI, + timeout: 20000 + } +}); diff --git a/apps/comments-ui/postcss.config.cjs b/apps/comments-ui/postcss.config.cjs new file mode 100644 index 0000000..ab7c493 --- /dev/null +++ b/apps/comments-ui/postcss.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/apps/comments-ui/src/actions.ts b/apps/comments-ui/src/actions.ts new file mode 100644 index 0000000..329a2b1 --- /dev/null +++ b/apps/comments-ui/src/actions.ts @@ -0,0 +1,544 @@ +import {AddComment, Comment, CommentsOptions, DispatchActionType, EditableAppContext, OpenCommentForm} from './app-context'; +import {AdminApi} from './utils/admin-api'; +import {GhostApi} from './utils/api'; +import {Page} from './pages'; + +async function loadMoreComments({state, api, options, order}: {state: EditableAppContext, api: GhostApi, options: CommentsOptions, order?:string}): Promise> { + let page = 1; + if (state.pagination && state.pagination.page) { + page = state.pagination.page + 1; + } + let data; + if (state.admin && state.adminApi) { + data = await state.adminApi.browse({page, postId: options.postId, order: order || state.order, memberUuid: state.member?.uuid}); + } else { + data = await api.comments.browse({page, postId: options.postId, order: order || state.order}); + } + + const updatedComments = [...state.comments, ...data.comments]; + const dedupedComments = updatedComments.filter((comment, index, self) => self.findIndex(c => c.id === comment.id) === index); + + // Note: we store the comments from new to old, and show them in reverse order + return { + comments: dedupedComments, + pagination: data.meta.pagination + }; +} + +function setCommentsIsLoading({data: isLoading}: {data: boolean | null}) { + return { + commentsIsLoading: isLoading + }; +} + +async function setOrder({state, data: {order}, options, api, dispatchAction}: {state: EditableAppContext, data: {order: string}, options: CommentsOptions, api: GhostApi, dispatchAction: DispatchActionType}) { + dispatchAction('setCommentsIsLoading', true); + + try { + let data; + if (state.admin && state.adminApi) { + data = await state.adminApi.browse({page: 1, postId: options.postId, order, memberUuid: state.member?.uuid}); + } else { + data = await api.comments.browse({page: 1, postId: options.postId, order}); + } + + return { + comments: [...data.comments], + pagination: data.meta.pagination, + order, + commentsIsLoading: false + }; + } catch (error) { + console.error('Failed to set order:', error); // eslint-disable-line no-console + state.commentsIsLoading = false; + throw error; // Rethrow the error to allow upstream handling + } +} + +async function loadMoreReplies({state, api, data: {comment, limit}, isReply}: {state: EditableAppContext, api: GhostApi, data: {comment: Comment, limit?: number | 'all'}, isReply: boolean}): Promise> { + const fetchReplies = async (afterReplyId: string | undefined, requestLimit: number) => { + if (state.admin && state.adminApi && !isReply) { // we don't want the admin api to load reply data for replying to a reply, so we pass isReply: true + return await state.adminApi.replies({commentId: comment.id, afterReplyId, limit: requestLimit, memberUuid: state.member?.uuid}); + } else { + return await api.comments.replies({commentId: comment.id, afterReplyId, limit: requestLimit}); + } + }; + + let afterReplyId: string | undefined = comment.replies && comment.replies.length > 0 + ? comment.replies[comment.replies.length - 1]?.id + : undefined; + + let allComments: Comment[] = []; + + if (limit === 'all') { + let hasMore = true; + + while (hasMore) { + const data = await fetchReplies(afterReplyId, 100); + allComments.push(...data.comments); + hasMore = !!data.meta?.pagination?.next; + + if (data.comments && data.comments.length > 0) { + afterReplyId = data.comments[data.comments.length - 1]?.id; + } else { + // If no comments returned, stop pagination to prevent infinite loop + hasMore = false; + } + } + } else { + const data = await fetchReplies(afterReplyId, limit as number || 100); + allComments = data.comments; + } + + // Note: we store the comments from new to old, and show them in reverse order + return { + comments: state.comments.map((c) => { + if (c.id === comment.id) { + return { + ...comment, + replies: [...comment.replies, ...allComments] + }; + } + return c; + }) + }; +} + +async function addComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, data: AddComment}) { + const data = await api.comments.add({comment}); + const newComment = data.comments[0]; + + return { + comments: [newComment, ...state.comments], + commentCount: state.commentCount + 1, + commentIdToScrollTo: newComment.id + }; +} + +async function addReply({state, api, data: {reply, parent}}: {state: EditableAppContext, api: GhostApi, data: {reply: any, parent: any}}) { + const data = await api.comments.add({ + comment: {...reply, parent_id: parent.id} + }); + const newComment = data.comments[0]; + + const allReplies = await api.comments.replies({commentId: parent.id, limit: 'all'}); + + return { + comments: state.comments.map((c) => { + if (c.id === parent.id) { + return { + ...c, + replies: allReplies.comments, + count: { + ...c.count, + replies: allReplies.comments.length + } + }; + } + return c; + }), + commentCount: state.commentCount + 1, + commentIdToScrollTo: newComment.id + }; +} + +async function hideComment({state, data: comment}: {state: EditableAppContext, adminApi: any, data: {id: string}}) { + if (state.adminApi) { + await state.adminApi.hideComment(comment.id); + } + return { + comments: state.comments.map((c) => { + const replies = c.replies.map((r) => { + if (r.id === comment.id) { + return { + ...r, + status: 'hidden' + }; + } + + return r; + }); + + if (c.id === comment.id) { + return { + ...c, + status: 'hidden', + replies + }; + } + + return { + ...c, + replies + }; + }), + commentCount: state.commentCount - 1 + }; +} + +async function showComment({state, api, data: comment}: {state: EditableAppContext, api: GhostApi, adminApi: any, data: {id: string}}) { + if (state.adminApi) { + await state.adminApi.showComment({id: comment.id}); + } + // We need to refetch the comment, to make sure we have an up to date HTML content + // + all relations are loaded as the current member (not the admin) + let data; + if (state.admin && state.adminApi) { + data = await state.adminApi.read({commentId: comment.id, memberUuid: state.member?.uuid}); + } else { + data = await api.comments.read(comment.id); + } + + const updatedComment = data.comments[0]; + + return { + comments: state.comments.map((c) => { + const replies = c.replies.map((r) => { + if (r.id === comment.id) { + return updatedComment; + } + + return r; + }); + + if (c.id === comment.id) { + return updatedComment; + } + + return { + ...c, + replies + }; + }), + commentCount: state.commentCount + 1 + }; +} + +async function updateCommentLikeState({state, data: comment}: {state: EditableAppContext, data: {id: string, liked: boolean}}) { + return { + comments: state.comments.map((c) => { + const replies = c.replies.map((r) => { + if (r.id === comment.id) { + return { + ...r, + liked: comment.liked, + count: { + ...r.count, + likes: comment.liked ? r.count.likes + 1 : r.count.likes - 1 + } + }; + } + + return r; + }); + + if (c.id === comment.id) { + return { + ...c, + liked: comment.liked, + replies, + count: { + ...c.count, + likes: comment.liked ? c.count.likes + 1 : c.count.likes - 1 + } + }; + } + + return { + ...c, + replies + }; + }) + }; +} + +async function likeComment({api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) { + dispatchAction('updateCommentLikeState', {id: comment.id, liked: true}); + try { + await api.comments.like({comment}); + return {}; + } catch { + dispatchAction('updateCommentLikeState', {id: comment.id, liked: false}); + } +} + +async function unlikeComment({api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) { + dispatchAction('updateCommentLikeState', {id: comment.id, liked: false}); + + try { + await api.comments.unlike({comment}); + return {}; + } catch { + dispatchAction('updateCommentLikeState', {id: comment.id, liked: true}); + } +} + +async function reportComment({api, data: comment}: {api: GhostApi, data: {id: string}}) { + await api.comments.report({comment}); + + return {}; +} + +async function deleteComment({state, api, data: comment, dispatchAction}: {state: EditableAppContext, api: GhostApi, data: {id: string}, dispatchAction: DispatchActionType}) { + await api.comments.edit({ + comment: { + id: comment.id, + status: 'deleted' + } + }); + + // If we're deleting a top-level comment with no replies we refresh the + // whole comments section to maintain correct pagination + const commentToDelete = state.comments.find(c => c.id === comment.id); + if (commentToDelete && (!commentToDelete.replies || commentToDelete.replies.length === 0)) { + dispatchAction('setOrder', {order: state.order}); + return null; + } + + return { + comments: state.comments.map((topLevelComment) => { + // If the comment has replies we want to keep it so the replies are + // still visible, but mark the comment as deleted. Otherwise remove it. + if (topLevelComment.id === comment.id) { + if (topLevelComment.replies.length > 0) { + return { + ...topLevelComment, + status: 'deleted' + }; + } else { + return null; // Will be filtered out later + } + } + + const originalLength = topLevelComment.replies.length; + const updatedReplies = topLevelComment.replies.filter(reply => reply.id !== comment.id); + const hasDeletedReply = originalLength !== updatedReplies.length; + + const updatedTopLevelComment = { + ...topLevelComment, + replies: updatedReplies + }; + + // When a reply is deleted we need to update the parent's count so + // pagination displays the correct number of replies still to load + if (hasDeletedReply && topLevelComment.count?.replies) { + topLevelComment.count.replies = topLevelComment.count.replies - 1; + } + + return updatedTopLevelComment; + }).filter(Boolean), + commentCount: state.commentCount - 1 + }; +} + +async function editComment({state, api, data: {comment, parent}}: {state: EditableAppContext, api: GhostApi, data: {comment: Partial & {id: string}, parent?: Comment}}) { + const data = await api.comments.edit({ + comment + }); + comment = data.comments[0]; + + // Replace the comment in the state with the new one + return { + comments: state.comments.map((c) => { + if (parent && parent.id === c.id) { + return { + ...c, + replies: c.replies.map((r) => { + if (r.id === comment.id) { + return comment; + } + return r; + }) + }; + } else if (c.id === comment.id) { + return comment; + } + + return c; + }) + }; +} + +async function updateMember({data, state, api}: {data: {name: string, expertise: string}, state: EditableAppContext, api: GhostApi}) { + const {name, expertise} = data; + const patchData: {name?: string, expertise?: string} = {}; + + const originalName = state?.member?.name; + + if (name && originalName !== name) { + patchData.name = name; + } + + const originalExpertise = state?.member?.expertise; + if (expertise !== undefined && originalExpertise !== expertise) { + // Allow to set it to an empty string or to null + patchData.expertise = expertise; + } + + if (Object.keys(patchData).length > 0) { + try { + const member = await api.member.update(patchData); + if (!member) { + throw new Error('Failed to update member'); + } + return { + member, + success: true + }; + } catch (err) { + return { + success: false, + error: err + }; + } + } + return null; +} + +function openPopup({data}: {data: Page}) { + return { + popup: data + }; +} + +function closePopup() { + return { + popup: null + }; +} + +async function openCommentForm({data: newForm, api, state}: {data: OpenCommentForm, api: GhostApi, state: EditableAppContext}) { + let otherStateChanges = {}; + + // When opening a reply form, load all replies for the parent comment so the + // reply appears in the correct position after posting + const topLevelCommentId = newForm.parent_id || newForm.id; + if (newForm.type === 'reply' && !state.openCommentForms.some(f => f.id === topLevelCommentId || f.parent_id === topLevelCommentId)) { + const comment = state.comments.find(c => c.id === topLevelCommentId); + + if (comment) { + try { + const newCommentsState = await loadMoreReplies({state, api, data: {comment, limit: 'all'}, isReply: true}); + otherStateChanges = {...otherStateChanges, ...newCommentsState}; + } catch (e) { + // If loading replies fails, continue anyway - the form should still open + // and replies will be loaded when the user submits + console.error('[Comments] Failed to load replies before opening form:', e); // eslint-disable-line no-console + } + } + } + + // We want to keep the number of displayed forms to a minimum so when opening a + // new form, we close any existing forms that are empty or have had no changes + const openFormsAfterAutoclose = state.openCommentForms.filter(form => form.hasUnsavedChanges); + + // avoid multiple forms being open for the same id + // (e.g. if "Reply" is hit on two different replies, we don't want two forms open at the bottom of that comment thread) + const openFormIndexForId = openFormsAfterAutoclose.findIndex(form => form.id === newForm.id); + if (openFormIndexForId > -1) { + openFormsAfterAutoclose[openFormIndexForId] = newForm; + return {openCommentForms: openFormsAfterAutoclose, ...otherStateChanges}; + } else { + return {openCommentForms: [...openFormsAfterAutoclose, newForm], ...otherStateChanges}; + } +} + +function setHighlightComment({data: commentId}: {data: string | null}) { + return { + commentIdToHighlight: commentId + }; +} + +function highlightComment({ + data: {commentId}, + dispatchAction + +}: { + data: { commentId: string | null }; + state: EditableAppContext; + dispatchAction: DispatchActionType; +}) { + setTimeout(() => { + dispatchAction('setHighlightComment', null); + }, 3000); + return { + commentIdToHighlight: commentId + }; +} + +function setCommentFormHasUnsavedChanges({data: {id, hasUnsavedChanges}, state}: {data: {id: string, hasUnsavedChanges: boolean}, state: EditableAppContext}) { + const updatedForms = state.openCommentForms.map((f) => { + if (f.id === id) { + return {...f, hasUnsavedChanges}; + } else { + return {...f}; + }; + }); + + return {openCommentForms: updatedForms}; +} + +function closeCommentForm({data: id, state}: {data: string, state: EditableAppContext}) { + return {openCommentForms: state.openCommentForms.filter(f => f.id !== id)}; +}; + +function setScrollTarget({data: commentId}: {data: string | null}) { + return {commentIdToScrollTo: commentId}; +} + +// Sync actions make use of setState((currentState) => newState), to avoid 'race' conditions +export const SyncActions = { + openPopup, + closePopup, + closeCommentForm, + setCommentFormHasUnsavedChanges, + setScrollTarget +}; + +export type SyncActionType = keyof typeof SyncActions; + +export const Actions = { + addComment, + editComment, + hideComment, + deleteComment, + showComment, + likeComment, + unlikeComment, + reportComment, + addReply, + loadMoreComments, + loadMoreReplies, + openCommentForm, + updateMember, + setOrder, + highlightComment, + setHighlightComment, + setCommentsIsLoading, + updateCommentLikeState +}; + +export type ActionType = keyof typeof Actions; + +export function isSyncAction(action: string): action is SyncActionType { + return !!(SyncActions as any)[action]; +} + +/** Handle actions in the App, returns updated state */ +export async function ActionHandler({action, data, state, api, adminApi, options, dispatchAction}: {action: ActionType, data: any, state: EditableAppContext, options: CommentsOptions, api: GhostApi, adminApi: AdminApi, dispatchAction: DispatchActionType}): Promise> { + const handler = Actions[action]; + if (handler) { + return await handler({data, state, api, adminApi, options, dispatchAction} as any) || {}; + } + return {}; +} + +/** Handle actions in the App, returns updated state */ +export function SyncActionHandler({action, data, state, api, adminApi, options}: {action: SyncActionType, data: any, state: EditableAppContext, options: CommentsOptions, api: GhostApi, adminApi: AdminApi}): Partial { + const handler = SyncActions[action]; + if (handler) { + // Do not await here + return handler({data, state, api, adminApi, options} as any) || {}; + } + return {}; +} diff --git a/apps/comments-ui/src/app-context.ts b/apps/comments-ui/src/app-context.ts new file mode 100644 index 0000000..49f178e --- /dev/null +++ b/apps/comments-ui/src/app-context.ts @@ -0,0 +1,132 @@ +// Ref: https://reactjs.org/docs/context.html +import React, {useContext} from 'react'; +import {ActionType, Actions, SyncActionType, SyncActions} from './actions'; +import {AdminApi} from './utils/admin-api'; +import {Page} from './pages'; + +export type Member = { + id: string, + uuid: string, + name: string, + avatar_image: string, + expertise: string, + can_comment?: boolean +} + +export type Comment = { + id: string, + post_id: string, + parent_id?: string, + in_reply_to_id: string, + in_reply_to_snippet: string, + replies: Comment[], + status: string, + liked: boolean, + count: { + replies: number, + likes: number, + }, + member: Member | null, + edited_at: string, + created_at: string, + html: string | null +} + +export type OpenCommentForm = { + id: string, + parent_id?: string, + in_reply_to_id?: string, + in_reply_to_snippet?: string, + type: 'reply' | 'edit', + hasUnsavedChanges: boolean +} + +export type AddComment = { + post_id: string, + status: string, + html: string +} + +export type LabsContextType = { + [key: string]: boolean | undefined +} + +export type CommentsOptions = { + locale: string, + siteUrl: string, + apiKey: string | undefined, + apiUrl: string | undefined, + postId: string, + adminUrl: string | undefined, + colorScheme: string | undefined, + avatarSaturation: number | undefined, + accentColor: string, + commentsEnabled: string | undefined, + title: string | null, + showCount: boolean, + publication: string +}; + +export type EditableAppContext = { + initStatus: string, + member: null | any, + admin: null | any, + comments: Comment[], + pagination: { + page: number, + limit: number, + pages: number, + total: number + } | null, + commentCount: number, + openCommentForms: OpenCommentForm[], + popup: Page | null, + labs: LabsContextType, + order: string, + adminApi: AdminApi | null, + commentsIsLoading?: boolean, + commentIdToHighlight: string | null, + commentIdToScrollTo: string | null, + showMissingCommentNotice: boolean, + pageUrl: string, + supportEmail: string | null, + isMember: boolean, + isAdmin: boolean, + isPaidOnly: boolean, + hasRequiredTier: boolean, + isCommentingDisabled: boolean +} + +export type TranslationFunction = (key: string, replacements?: Record) => string; + +export type AppContextType = EditableAppContext & CommentsOptions & { + // This part makes sure we can add automatic data and return types to the actions when using context.dispatchAction('actionName', data) + t: TranslationFunction, + dispatchAction: (action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends { data: any } ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : any) => T extends ActionType ? Promise : void, + openFormCount: number +} + +// Copy time from AppContextType +export type DispatchActionType = AppContextType['dispatchAction']; +export const AppContext = React.createContext({} as any); + +export const AppContextProvider = AppContext.Provider; + +export const useAppContext = () => useContext(AppContext); + +export const useOrderChange = () => { + const context = useAppContext(); + const dispatchAction = context.dispatchAction; + return (order: string) => { + dispatchAction('setOrder', {order}); + }; +}; + +export const useLabs = () => { + try { + const context = useAppContext(); + return context.labs || {}; + } catch { + return {}; + } +}; diff --git a/apps/comments-ui/src/app.tsx b/apps/comments-ui/src/app.tsx new file mode 100644 index 0000000..8f1ac40 --- /dev/null +++ b/apps/comments-ui/src/app.tsx @@ -0,0 +1,363 @@ +/* eslint-disable no-shadow */ + +import AuthFrame from './auth-frame'; +import ContentBox from './components/content-box'; +import PopupBox from './components/popup-box'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import i18nLib from '@tryghost/i18n'; +import setupGhostApi from './utils/api'; +import {ActionHandler, SyncActionHandler, isSyncAction} from './actions'; +import {AppContext, Comment, DispatchActionType, EditableAppContext} from './app-context'; +import {CommentsFrame} from './components/frame'; +import {setupAdminAPI} from './utils/admin-api'; +import {useOptions} from './utils/options'; + +type AppProps = { + scriptTag: HTMLElement; + initialCommentId: string | null; + pageUrl: string; +}; + +const ALLOWED_MODERATORS = ['Owner', 'Administrator', 'Super Editor']; + +/** + * Check if a comment ID exists in the comments array (either as a top-level comment or reply) + */ +function isCommentLoaded(comments: Comment[], targetId: string): boolean { + return comments.some(c => c.id === targetId || c.replies?.some(r => r.id === targetId)); +} + +const App: React.FC = ({scriptTag, initialCommentId, pageUrl}) => { + const options = useOptions(scriptTag); + const [state, setFullState] = useState({ + initStatus: 'running', + member: null, + admin: null, + comments: [], + pagination: null, + commentCount: 0, + openCommentForms: [], + popup: null, + labs: {}, + order: 'count__likes desc, created_at desc', + adminApi: null, + commentsIsLoading: false, + commentIdToHighlight: null, + commentIdToScrollTo: initialCommentId, + showMissingCommentNotice: false, + pageUrl, + supportEmail: null, + isMember: false, + isAdmin: false, + isPaidOnly: false, + hasRequiredTier: true, + isCommentingDisabled: false + }); + + const iframeRef = React.createRef(); + + const api = React.useMemo(() => { + return setupGhostApi({ + siteUrl: options.siteUrl, + apiUrl: options.apiUrl!, + apiKey: options.apiKey! + }); + }, [options]); + + const setState = useCallback((newState: Partial | ((state: EditableAppContext) => Partial)) => { + setFullState((state) => { + if (typeof newState === 'function') { + newState = newState(state); + } + return { + ...state, + ...newState + }; + }); + }, [setFullState]); + + const dispatchAction = useCallback(async (action, data) => { + if (isSyncAction(action)) { + // Makes sure we correctly handle the old state + // because updates to state may be asynchronous + // so calling dispatchAction('counterUp') multiple times, may yield unexpected results if we don't use a callback function + setState((state) => { + return SyncActionHandler({action, data, state, api, adminApi: state.adminApi!, options}); + }); + return; + } + + // This is a bit a ugly hack, but only reliable way to make sure we can get the latest state asynchronously + // without creating infinite rerenders because dispatchAction needs to change on every state change + // So state shouldn't be a dependency of dispatchAction + // + // Wrapped in a Promise so that callers of `dispatchAction` can await the action completion. setState doesn't + // allow for async actions within it's updater function so this is the best option. + return new Promise((resolve) => { + setState((state) => { + ActionHandler({action, data, state, api, adminApi: state.adminApi!, options, dispatchAction: dispatchAction as DispatchActionType}).then((updatedState) => { + const newState = {...updatedState}; + resolve(newState); + setState(newState); + }).catch(console.error); // eslint-disable-line no-console + + // No immediate changes + return {}; + }); + }); + }, [api, options]); // Do not add state or context as a dependency here -> infinite render loop + + const i18n = useMemo(() => { + return i18nLib(options.locale, 'comments'); + }, [options.locale]); + + const context = { + ...options, + ...state, + t: i18n.t, + dispatchAction: dispatchAction as DispatchActionType, + openFormCount: useMemo(() => state.openCommentForms.length, [state.openCommentForms]) + }; + + const initAdminAuth = async () => { + if (state.adminApi || !options.adminUrl) { + return; + } + + try { + const adminApi = setupAdminAPI({ + adminUrl: options.adminUrl + }); + + let admin = null; + try { + admin = await adminApi.getUser(); + + // remove 'admin' for any roles (author, contributor, editor) who can't moderate comments + if (!admin || !(admin.roles.some(role => ALLOWED_MODERATORS.includes(role.name)))) { + admin = null; + } + + if (admin) { + // this is a bit of a hack, but we need to fetch the comments fully populated if the user is an admin + const adminComments = await adminApi.browse({page: 1, postId: options.postId, order: state.order, memberUuid: state.member?.uuid}); + setState((currentState) => { + // Don't overwrite comments when initSetup loaded extra data + // for permalink scrolling (multiple pages or expanded replies) + if ((currentState.pagination && currentState.pagination.page > 1) || initialCommentId) { + return { + adminApi, + admin, + isAdmin: true + }; + } + return { + adminApi, + admin, + isAdmin: true, + comments: adminComments.comments, + pagination: adminComments.meta.pagination + }; + }); + } + } catch (e) { + // Loading of admin failed. Could be not signed in, or a different error (not important) + // eslint-disable-next-line no-console + console.warn(`[Comments] Failed to fetch admin endpoint:`, e); + } + + setState({ + adminApi, + admin, + isAdmin: !!admin + }); + } catch (e) { + /* eslint-disable no-console */ + console.error(`[Comments] Failed to initialize admin authentication:`, e); + } + }; + + /** Fetch first few comments */ + const fetchComments = async () => { + const dataPromise = api.comments.browse({page: 1, postId: options.postId, order: state.order}); + const countPromise = api.comments.count({postId: options.postId}); + + const [data, count] = await Promise.all([dataPromise, countPromise]); + + return { + comments: data.comments, + pagination: data.meta.pagination, + count: count + }; + }; + + /** + * Fetch the target comment and verify it exists and is published. + * Returns null if the comment doesn't exist or isn't accessible. + */ + const fetchScrollTarget = async (targetId: string): Promise => { + try { + const response = await api.comments.read(targetId); + const comment = response.comments?.[0]; + return (comment && comment.status === 'published') ? comment : null; + } catch { + return null; + } + }; + + /** + * Paginate through comments until the target (or its parent) is found. + */ + const paginateToComment = async ( + targetId: string, + parentId: string | undefined, + initialComments: Comment[], + initialPagination: {page: number; pages: number} + ): Promise<{comments: Comment[]; pagination: typeof initialPagination}> => { + let comments = initialComments; + let pagination = initialPagination; + + while (!isCommentLoaded(comments, targetId) && pagination.page < pagination.pages) { + if (parentId && comments.some(c => c.id === parentId)) { + break; + } + + const nextPage = await api.comments.browse({ + page: pagination.page + 1, + postId: options.postId, + order: state.order + }); + comments = [...comments, ...nextPage.comments]; + pagination = nextPage.meta.pagination; + } + + return {comments, pagination}; + }; + + /** + * Load additional comment pages and/or replies until the scroll + * target is found. After paginating to the parent comment, if the + * target reply isn't in the inline replies (partial API response), + * fetch all replies from the server. + */ + const loadScrollTarget = async ( + targetId: string, + targetComment: Comment, + initialComments: Comment[], + initialPagination: {page: number; pages: number} + ): Promise<{comments: Comment[]; pagination: typeof initialPagination; found: boolean}> => { + const parentId = targetComment.parent_id; + + const {comments: paginatedComments, pagination} = await paginateToComment(targetId, parentId, initialComments, initialPagination); + let comments = paginatedComments; + + if (parentId && !isCommentLoaded(comments, targetId)) { + const {comments: allReplies} = await api.comments.replies({commentId: parentId, limit: 'all'}); + comments = comments.map(c => (c.id === parentId ? {...c, replies: allReplies} : c)); + } + + return {comments, pagination, found: isCommentLoaded(comments, targetId)}; + }; + + /** Initialize comments setup once in viewport, fetch data and setup state */ + const initSetup = async () => { + try { + const {member, labs, supportEmail} = await api.init(); + const {count, comments: initialComments, pagination: initialPagination} = await fetchComments(); + + let comments = initialComments; + let pagination = initialPagination; + let scrollTargetFound = false; + + const shouldFindScrollTarget = initialCommentId && pagination; + if (shouldFindScrollTarget) { + const targetComment = await fetchScrollTarget(initialCommentId); + if (targetComment) { + const result = await loadScrollTarget(initialCommentId, targetComment, comments, pagination); + comments = result.comments; + pagination = result.pagination; + scrollTargetFound = result.found; + } + } + + // Compute tier access values + const isMember = !!member; + const isPaidOnly = options.commentsEnabled === 'paid'; + const isPaidMember = !!member?.paid; + const hasRequiredTier = isPaidMember || !isPaidOnly; + + setState({ + member, + initStatus: 'success', + comments, + pagination, + commentCount: count, + order: 'count__likes desc, created_at desc', + labs: labs, + commentsIsLoading: false, + commentIdToHighlight: null, + commentIdToScrollTo: scrollTargetFound ? initialCommentId : null, + showMissingCommentNotice: !!initialCommentId && !scrollTargetFound, + supportEmail, + isMember, + isPaidOnly, + hasRequiredTier, + isCommentingDisabled: member?.can_comment === false + }); + } catch (e) { + console.error(`[Comments] Failed to initialize:`, e); + /* eslint-enable no-console */ + setState({ + initStatus: 'failed' + }); + } + }; + + /** Delay initialization until comments block is in viewport (unless permalink present) */ + useEffect(() => { + // If we have a permalink, load immediately (skip lazy loading) + if (initialCommentId) { + initSetup(); + return; + } + + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + initSetup(); + if (iframeRef.current) { + observer.unobserve(iframeRef.current); + } + } + }); + }, { + root: null, + rootMargin: '0px', + threshold: 0.1 + }); + + if (iframeRef.current) { + observer.observe(iframeRef.current); + } + + return () => { + if (iframeRef.current) { + observer.unobserve(iframeRef.current); + } + }; + }, [iframeRef.current, initialCommentId]); + + const done = state.initStatus === 'success'; + + return ( + + + + + {state.comments.length > 0 ? : null} + + + ); +}; + +export default App; diff --git a/apps/comments-ui/src/auth-frame.tsx b/apps/comments-ui/src/auth-frame.tsx new file mode 100644 index 0000000..8410eac --- /dev/null +++ b/apps/comments-ui/src/auth-frame.tsx @@ -0,0 +1,14 @@ +type Props = { + adminUrl: string|undefined; + onLoad: () => void; +}; +const AuthFrame: React.FC = ({adminUrl, onLoad}) => { + const iframeStyle = { + display: 'none' + }; + + return ( + + ); +}; +export default AuthFrame; diff --git a/apps/comments-ui/src/index.tsx b/apps/comments-ui/src/index.tsx new file mode 100644 index 0000000..18ce5bb --- /dev/null +++ b/apps/comments-ui/src/index.tsx @@ -0,0 +1,75 @@ +import App from './app'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import {ROOT_DIV_ID} from './utils/constants'; +import {parseCommentIdFromHash} from './utils/helpers'; + +function getScriptTag(): HTMLElement { + let scriptTag = document.currentScript as HTMLElement | null; + + if (!scriptTag && import.meta.env.DEV) { + // In development mode, use any script tag (because in ESM mode, document.currentScript is not set) + scriptTag = document.querySelector('script[data-ghost-comments]'); + } + + if (!scriptTag) { + throw new Error('[Comments-UI] Cannot find current script tag'); + } + + return scriptTag; +} + +/** + * Returns a div to mount the React application into, creating it if necessary + */ +function getRootDiv(scriptTag: HTMLElement) { + if (scriptTag.previousElementSibling && scriptTag.previousElementSibling.id === ROOT_DIV_ID) { + return scriptTag.previousElementSibling; + } + + if (!scriptTag.parentElement) { + throw new Error('[Comments-UI] Script tag does not have a parent element'); + } + + const elem = document.createElement('div'); + elem.id = ROOT_DIV_ID; + scriptTag.parentElement.insertBefore(elem, scriptTag); + return elem; +} + +function handleTokenUrl() { + const url = new URL(window.location.href); + if (url.searchParams.get('token')) { + url.searchParams.delete('token'); + window.history.replaceState({}, document.title, url.href); + } +} + +function getPageUrl(): string { + const url = new URL(window.location.href); + url.hash = ''; + return url.toString(); +} + +function init() { + const scriptTag = getScriptTag(); + const root = getRootDiv(scriptTag); + const initialCommentId = parseCommentIdFromHash(window.location.hash); + const pageUrl = getPageUrl(); + + try { + handleTokenUrl(); + + ReactDOM.render( + + {} + , + root + ); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +} + +init(); diff --git a/apps/comments-ui/src/pages.ts b/apps/comments-ui/src/pages.ts new file mode 100644 index 0000000..a29af31 --- /dev/null +++ b/apps/comments-ui/src/pages.ts @@ -0,0 +1,29 @@ +import AddDetailsPopup from './components/popups/add-details-popup'; +import CTAPopup from './components/popups/cta-popup'; +import DeletePopup from './components/popups/delete-popup'; +import React from 'react'; +import ReportPopup from './components/popups/report-popup'; + +/** List of all available pages in Comments-UI, mapped to their UI component + * Any new page added to comments-ui needs to be mapped here +*/ +export const Pages = { + addDetailsPopup: AddDetailsPopup, + reportPopup: ReportPopup, + ctaPopup: CTAPopup, + deletePopup: DeletePopup +}; +export type PageName = keyof typeof Pages; + +type PageTypes = { + [name in PageName]: { + type: name, + /** + * Called when closing the popup + * @param succeeded False if normal cancel/close buttons are used + */ + callback?: (succeeded: boolean) => void, + } & React.ComponentProps +} + +export type Page = PageTypes[keyof PageTypes] diff --git a/apps/comments-ui/src/setup-tests.ts b/apps/comments-ui/src/setup-tests.ts new file mode 100644 index 0000000..3d74702 --- /dev/null +++ b/apps/comments-ui/src/setup-tests.ts @@ -0,0 +1,17 @@ +import {afterEach} from 'vitest'; +import {cleanup} from '@testing-library/react'; +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; + +afterEach(() => { + cleanup(); +}); + +global.ResizeObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn() +})); diff --git a/apps/comments-ui/src/typings.d.ts b/apps/comments-ui/src/typings.d.ts new file mode 100644 index 0000000..f1ae460 --- /dev/null +++ b/apps/comments-ui/src/typings.d.ts @@ -0,0 +1,7 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; + } diff --git a/apps/comments-ui/src/vite-env.d.ts b/apps/comments-ui/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/comments-ui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/comments-ui/tailwind.config.js b/apps/comments-ui/tailwind.config.js new file mode 100644 index 0000000..e3192c8 --- /dev/null +++ b/apps/comments-ui/tailwind.config.js @@ -0,0 +1,190 @@ +module.exports = { + darkMode: 'class', + theme: { + extend: { + animation: { + heartbeat: 'heartbeat 0.35s ease-in-out forwards', + pulse: 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' + }, + keyframes: { + heartbeat: { + '0%, 100%': {transform: 'scale(1)'}, + '50%': {transform: 'scale(1.3)'} + } + } + }, + screens: { + sm: '481px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1400px' + }, + spacing: { + px: '1px', + 0: '0px', + 0.5: '0.2rem', + 1: '0.4rem', + 1.5: '0.6rem', + 2: '0.8rem', + 2.5: '1rem', + 3: '1.2rem', + 3.5: '1.4rem', + 4: '1.6rem', + 5: '2rem', + 6: '2.4rem', + 7: '2.8rem', + 8: '3.2rem', + 9: '3.6rem', + 10: '4rem', + 11: '4.4rem', + 12: '4.8rem', + 14: '5.6rem', + 16: '6.4rem', + 20: '8rem', + 24: '9.6rem', + 28: '11.2rem', + 32: '12.8rem', + 36: '14.4rem', + 40: '16rem', + 44: '17.6rem', + 48: '19.2rem', + 52: '20.8rem', + 56: '22.4rem', + 60: '24rem', + 64: '25.6rem', + 72: '28.8rem', + 80: '32rem', + 96: '38.4rem' + }, + maxWidth: { + none: 'none', + 0: '0rem', + xs: '32rem', + sm: '38.4rem', + md: '44.8rem', + lg: '51.2rem', + xl: '57.6rem', + '2xl': '67.2rem', + '3xl': '76.8rem', + '4xl': '89.6rem', + '5xl': '102.4rem', + '6xl': '115.2rem', + '7xl': '128rem', + '8xl': '140rem', + '9xl': '156rem', + full: '100%', + min: 'min-content', + max: 'max-content', + fit: 'fit-content', + prose: '65ch' + }, + minWidth: { + none: 'none', + 0: '0rem', + xs: '32rem', + sm: '38.4rem', + md: '44.8rem', + lg: '51.2rem', + xl: '57.6rem', + '2xl': '67.2rem', + '3xl': '76.8rem', + '4xl': '89.6rem', + '5xl': '102.4rem', + '6xl': '115.2rem', + '7xl': '128rem', + '8xl': '140rem', + '9xl': '156rem', + full: '100%', + min: 'min-content', + max: 'max-content', + fit: 'fit-content', + prose: '65ch' + }, + borderRadius: { + sm: '0.2rem', + DEFAULT: '0.4rem', + md: '0.6rem', + lg: '0.8rem', + xl: '1.2rem', + '2xl': '1.6rem', + '3xl': '2.4rem', + full: '9999px' + }, + fontSize: { + xs: '1.2rem', + base: '1.3rem', + sm: '1.4rem', + md: '1.5rem', + lg: '1.65rem', + xl: '2rem', + '2xl': '2.4rem', + '3xl': '3rem', + '4xl': '3.6rem', + '5xl': ['4.8rem', '1.15'], + '6xl': ['6rem', '1'], + '7xl': ['7.2rem', '1'], + '8xl': ['9.6rem', '1'], + '9xl': ['12.8rem', '1'] + }, + letterSpacing: { + tightest: '-.075em', + tighter: '-.05em', + tight: '-.018em', + normal: '0', + wide: '.018em', + wider: '.05em', + widest: '.1em' + }, + boxShadow: { + lg: [ + 'rgba(0, 0, 0, 0.06) 0px 0px 0px 1px', + 'rgba(0, 0, 0, 0.04) 0px 2px 2px -1px', + 'rgba(0, 0, 0, 0.04) 0px 3px 3px -1px', + 'rgba(0, 0, 0, 0.03) 0px 5px 5px -2px', + 'rgba(0, 0, 0, 0.03) 0px 10px 10px -3px', + 'rgba(0, 0, 0, 0.03) 0px 24px 24px -8px' + ], + xl: [ + '0px 0px 1px rgba(0, 0, 0, 0.12)', + '0px 13px 20px rgba(0, 0, 0, 0.04)', + '0px 14px 57px rgba(0, 0, 0, 0.06)' + ], + form: [ + '0px 78px 57px -57px rgba(0, 0, 0, 0.1)', + '0px 15px 20px -8px rgba(0, 0, 0, 0.08)', + '0px 0px 1px 0px rgba(0,0,0,0.32)' + ], + formxl: [ + '0px 78px 57px -57px rgba(0, 0, 0, 0.125)', + '0px 15px 20px -8px rgba(0, 0, 0, 0.1)', + '0px 0px 1px 0px rgba(0, 0, 0, 0.32)' + ], + modal: [ + '0 3.8px 2.2px rgba(0, 0, 0, 0.028)', + '0 9.2px 5.3px rgba(0, 0, 0, 0.04)', + '0 17.3px 10px rgba(0, 0, 0, 0.05)', + '0 30.8px 17.9px rgba(0, 0, 0, 0.06)', + '0 57.7px 33.4px rgba(0, 0, 0, 0.072)', + '0 138px 80px rgba(0, 0, 0, 0.1)' + ] + }, + animation: { + heartbeat: 'heartbeat 0.35s ease-in-out forwards', + highlight: 'highlight 1s steps(1) forwards' + }, + keyframes: { + heartbeat: { + '0%, 100%': {transform: 'scale(1)'}, + '50%': {transform: 'scale(1.3)'} + }, + highlight: { + '100%': {backgroundColor: 'transparent'} + } + } + }, + content: [ + './src/**/*.{js,jsx,ts,tsx}' + ], + plugins: [] +}; diff --git a/apps/comments-ui/tsconfig.json b/apps/comments-ui/tsconfig.json new file mode 100644 index 0000000..b7fc8ae --- /dev/null +++ b/apps/comments-ui/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + /* Vitest */ + "types": ["vitest/globals"] + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/apps/comments-ui/tsconfig.node.json b/apps/comments-ui/tsconfig.node.json new file mode 100644 index 0000000..e2db264 --- /dev/null +++ b/apps/comments-ui/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.mts", "vite-plugin-strip-fingerprinting.ts", "package.json"] +} diff --git a/apps/comments-ui/vite-plugin-strip-fingerprinting.ts b/apps/comments-ui/vite-plugin-strip-fingerprinting.ts new file mode 100644 index 0000000..ab5274c --- /dev/null +++ b/apps/comments-ui/vite-plugin-strip-fingerprinting.ts @@ -0,0 +1,191 @@ +import type {Plugin} from 'vite'; + +interface Replacement { + search: string; + replace: string; + description: string; +} + +interface PatternGroup { + filePattern: RegExp; + replacements: Replacement[]; +} + +/** + * Vite plugin that patches ProseMirror and tiptap browser detection to avoid + * accessing high-entropy fingerprinting APIs (navigator.vendor, + * navigator.platform, navigator.maxTouchPoints). + * + * DuckDuckGo's Tracker Radar classifies scripts that access these APIs as + * fingerprinting (score 3 = maximum). Safari's Advanced Fingerprinting + * Protection (on by default since Safari 26, Sept 2025) uses this data to + * restrict API access and storage for scripts from flagged domains like + * cdn.jsdelivr.net. + * + * This plugin replaces those API accesses with equivalent checks using only + * navigator.userAgent, which has a much lower fingerprinting weight. + * + * Affected packages: + * - prosemirror-view: navigator.vendor, navigator.platform, navigator.maxTouchPoints + * - prosemirror-keymap: navigator.platform + * - prosemirror-commands: navigator.platform + * - @tiptap/core: navigator.platform + * - w3c-keyname: navigator.platform + */ +export function stripFingerprintingPlugin(): Plugin { + const patternGroups: PatternGroup[] = [ + { + filePattern: /prosemirror-view[\\/]dist[\\/]index\.js$/, + replacements: [ + { + // Safari detection: nav.vendor → userAgent check + // Original: checks if vendor is "Apple Computer" + // Patched: checks UA for Safari without Chrome/Chromium + search: '/Apple Computer/.test(nav.vendor)', + replace: '/Safari\\//.test(agent) && !/Chrome\\//.test(agent) && !/Chromium\\//.test(agent)', + description: 'prosemirror-view: safari detection (nav.vendor)' + }, + { + // iOS detection: remove nav.maxTouchPoints fallback + // Original: detects iPadOS via maxTouchPoints > 2 + // Patched: relies on Mobile/xxx in UA only + // Trade-off: iPadOS 13+ sends desktop Mac UA, so it won't + // be detected as iOS. This is acceptable — iPad works fine + // with desktop Mac editor handling. + search: ' || !!nav && nav.maxTouchPoints > 2', + replace: '', + description: 'prosemirror-view: iOS detection (nav.maxTouchPoints)' + }, + { + // Mac detection: nav.platform → userAgent check + search: 'nav ? /Mac/.test(nav.platform) : false', + replace: '/Macintosh/.test(agent)', + description: 'prosemirror-view: mac detection (nav.platform)' + }, + { + // Windows detection: nav.platform → userAgent check + search: 'nav ? /Win/.test(nav.platform) : false', + replace: '/Windows/.test(agent)', + description: 'prosemirror-view: windows detection (nav.platform)' + } + ] + }, + { + filePattern: /prosemirror-keymap[\\/]dist[\\/]index\.js$/, + replacements: [ + { + search: '/Mac|iP(hone|[oa]d)/.test(navigator.platform)', + replace: '/Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent)', + description: 'prosemirror-keymap: mac/iOS detection (navigator.platform)' + } + ] + }, + { + filePattern: /prosemirror-commands[\\/]dist[\\/]index\.js$/, + replacements: [ + { + search: '/Mac|iP(hone|[oa]d)/.test(navigator.platform)', + replace: '/Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent)', + description: 'prosemirror-commands: mac/iOS detection (navigator.platform)' + } + ] + }, + { + filePattern: /w3c-keyname[\\/]index\.js$/, + replacements: [ + { + search: '/Mac/.test(navigator.platform)', + replace: '/Macintosh/.test(navigator.userAgent)', + description: 'w3c-keyname: mac detection (navigator.platform)' + } + ] + }, + { + filePattern: /@tiptap[\\/]core[\\/]dist[\\/]index\.js$/, + replacements: [ + { + // isAndroid: remove navigator.platform === 'Android' check, + // keep the userAgent fallback which already handles this + search: 'navigator.platform === \'Android\' || ', + replace: '', + description: '@tiptap/core: isAndroid (navigator.platform)' + }, + { + // isiOS: replace navigator.platform array check with UA + // The array ['iPad Simulator', 'iPhone Simulator', ...] is + // still present but .includes() on it becomes a no-op. + // The UA check catches real iPhone/iPod devices. + // iPadOS 13+ is handled by the next line in tiptap: + // navigator.userAgent.includes('Mac') && 'ontouchend' in document + search: '].includes(navigator.platform)', + replace: '].length === 0 || /iPhone|iPod/.test(navigator.userAgent)', + description: '@tiptap/core: isiOS (navigator.platform)' + }, + { + // isMacOS: replace navigator.platform with userAgent + search: '/Mac/.test(navigator.platform)', + replace: '/Macintosh/.test(navigator.userAgent)', + description: '@tiptap/core: isMacOS (navigator.platform)' + } + ] + } + ]; + + const appliedReplacements = new Map>(); + + return { + name: 'strip-fingerprinting', + enforce: 'pre', + + buildStart() { + appliedReplacements.clear(); + }, + + transform(code: string, id: string) { + const normalizedId = id.replace(/\\/g, '/'); + + const group = patternGroups.find(g => g.filePattern.test(normalizedId)); + if (!group) { + return null; + } + + let transformed = code; + let hasChanges = false; + + for (const replacement of group.replacements) { + if (transformed.includes(replacement.search)) { + transformed = transformed.replaceAll(replacement.search, replacement.replace); + hasChanges = true; + + if (!appliedReplacements.has(normalizedId)) { + appliedReplacements.set(normalizedId, new Set()); + } + appliedReplacements.get(normalizedId)!.add(replacement.description); + } + } + + if (hasChanges) { + return {code: transformed, map: null}; + } + + return null; + }, + + buildEnd() { + const allDescriptions = patternGroups.flatMap(g => g.replacements.map(r => r.description)); + const applied = new Set( + [...appliedReplacements.values()].flatMap(s => [...s]) + ); + + const missing = allDescriptions.filter(d => !applied.has(d)); + + if (missing.length > 0) { + this.warn( + `strip-fingerprinting: ${missing.length} replacement(s) did not match. ` + + `Dependencies may have been updated. Unmatched:\n` + + missing.map(d => ` - ${d}`).join('\n') + ); + } + } + }; +} diff --git a/apps/comments-ui/vite.config.mts b/apps/comments-ui/vite.config.mts new file mode 100644 index 0000000..b2b6889 --- /dev/null +++ b/apps/comments-ui/vite.config.mts @@ -0,0 +1,91 @@ +import pkg from './package.json'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; +import {SUPPORTED_LOCALES} from '@tryghost/i18n'; +import {defineConfig} from 'vitest/config'; +import {resolve} from 'path'; +import {stripFingerprintingPlugin} from './vite-plugin-strip-fingerprinting'; + +const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name; + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + stripFingerprintingPlugin(), + svgr(), + react() + ], + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.VITEST_SEGFAULT_RETRY': 3 + }, + preview: { + host: '0.0.0.0', + allowedHosts: true, // allows domain-name proxies to the preview server + port: 7173, + cors: true + }, + server: { + port: 5368 + }, + build: { + reportCompressedSize: false, + outDir: resolve(__dirname, 'umd'), + emptyOutDir: true, + minify: true, + sourcemap: true, + cssCodeSplit: true, + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + formats: ['umd'], + name: pkg.name, + fileName(format) { + if (format === 'umd') { + return `${outputFileName}.min.js`; + } + + return `${outputFileName}.js`; + } + }, + rollupOptions: { + output: {} + }, + commonjsOptions: { + include: [/ghost/, /node_modules/], + dynamicRequireRoot: '../../', + dynamicRequireTargets: SUPPORTED_LOCALES.map(locale => `../../ghost/i18n/locales/${locale}/comments.json`) + } + }, + resolve: { + // comments-ui uses React 17 while the monorepo hoists React 18; + // dedupe + alias ensures all deps (including @tiptap/react) use + // the same React 17 instance from comments-ui's node_modules + dedupe: ['react', 'react-dom', '@tryghost/debug'], + alias: { + 'react': resolve(__dirname, 'node_modules/react'), + 'react-dom': resolve(__dirname, 'node_modules/react-dom') + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + setupFiles: './src/setup-tests.ts', + include: ['test/unit/**/*.test.{js,jsx,ts,tsx}'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + server: { + deps: { + // Inline all deps so Vite's resolve.alias applies to their + // React imports (prevents duplicate React 17 instances when + // the monorepo hoists React 18) + inline: [/@tiptap/, /@headlessui/] + } + }, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); +}); diff --git a/apps/portal/.env b/apps/portal/.env new file mode 100644 index 0000000..0454adf --- /dev/null +++ b/apps/portal/.env @@ -0,0 +1 @@ +REACT_APP_VERSION=$npm_package_version diff --git a/apps/portal/.env.development.local.example b/apps/portal/.env.development.local.example new file mode 100644 index 0000000..9b901e7 --- /dev/null +++ b/apps/portal/.env.development.local.example @@ -0,0 +1 @@ +REACT_APP_DEFAULT_PAGE=signup diff --git a/apps/portal/.eslintignore b/apps/portal/.eslintignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/apps/portal/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/apps/portal/LICENSE b/apps/portal/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/apps/portal/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/portal/README.md b/apps/portal/README.md new file mode 100644 index 0000000..cc90859 --- /dev/null +++ b/apps/portal/README.md @@ -0,0 +1,103 @@ +# Portal + +[![npm version](https://badge.fury.io/js/%40tryghost%2Fportal.svg)](https://badge.fury.io/js/%40tryghost%2Fportal) + +[Drop-in script](https://ghost.org/help/setting-up-portal/) to make the bulk of Ghost membership features work on any theme. + +## Usage + +Ghost automatically injects Portal script on all sites running Ghost 4 or higher. + +Alternatively, Portal can be enabled on non-ghost pages directly by inserting the below script on the page. + +```html + +``` + +The `data-ghost` attribute expects the URL for your Ghost site, which is the only input Portal needs to work with your site's membership data via Ghost APIs. + +### Custom trigger button + +By default, the script adds a default floating trigger button on the bottom right of your page which is used to trigger the popup on screen. + +Its possible to add custom trigger button of your own by adding data attribute `data-portal` to any HTML tag on page, and also specify a specific [page](https://github.com/TryGhost/Ghost/blob/main/ghost/portal/src/pages.js#L13-L22) to open from it by using it as `data-portal=signup`. + +Share modal can be opened with `data-portal="share"` (or `#/share`). + +Default (zero-config) usage: +```html + +``` + +On pages where `{{ghost_head}}` is rendered, Portal will auto-resolve metadata from DOM tags: +- URL: canonical URL (or current URL fallback) +- Title: Open Graph title (or document title fallback) +- Image: Open Graph image (or Twitter image fallback) + +Troubleshooting missing preview metadata: +1. Verify the template includes `{{ghost_head}}`. +2. Verify rendered HTML contains canonical + OG/Twitter tags. + +The script also adds custom class names to this element for open and close state of popup - `gh-portal-open` and `gh-portal-close`, allowing devs to update its UI based on popup state. + +Refer the [docs](https://ghost.org/help/setup-members/#customize-portal-settings) to read about ways in which Portal can be customized for your site. + +## Develop + +Portal runs automatically when using Ghost's development command from the monorepo root: +``` +pnpm dev +``` + +This starts all frontend apps (including Portal.) +--- + +To run Portal in a standalone fashion, use `pnpm preview` and open [http://localhost:3000](http://localhost:3000). + +## Build + +To create a production minified bundle in `umd/portal.min.js`: +``` +pnpm build +``` + +## Test + +To run tests in watch mode: +``` +pnpm test +``` + +### Ghost e2e tests + +Portal is primarily tested via Ghost's e2e Playwright tests in the `e2e/` directory. Run them from the monorepo root: +``` +pnpm test:e2e +``` + +## Release + +A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released. In either case, you need sufficient permissions to release `@tryghost` packages on NPM. + +If you're releasing new code that should not immediately go live _always_ use a minor or major version when publishing. + +In order to have Ghost's e2e tests run against the new code on CI or to test the new code in staging, you need to publish to npm following the Minor / major release process below. + +### Patch release + +1. Run `pnpm ship` and select a patch version when prompted +2. Merge the release commit to `main` + +### Minor / major release + +1. Run `pnpm ship` and select a minor or major version when prompted +2. Merge the release commit to `main` +3. Wait until a new version of Ghost is released + +### JsDelivr cache +If the CI doesn't clear JsDelivr cache to get the new version out instantly, you may want to do it yourself manually ([docs](https://www.notion.so/ghost/How-to-clear-jsDelivr-CDN-cache-2930bdbac02946eca07ac23ab3199bfa?pvs=4)). Typically, you'll need to open `https://purge.jsdelivr.net/ghost/portal@~${PORTAL_VERSION}/umd/portal.min.js` and +`https://purge.jsdelivr.net/ghost/portal@~${PORTAL_VERSION}/umd/main.css` in your browser, where `PORTAL_VERSION` is the latest minor version in `ghost/core/core/shared/config/defaults.json` ([code](https://github.com/TryGhost/Ghost/blob/0aef3d3beeebcd79a4bfd3ad27e0ac67554b5744/ghost/core/core/shared/config/defaults.json#L185)) + +# Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/apps/portal/jsconfig.json b/apps/portal/jsconfig.json new file mode 100644 index 0000000..5f2e866 --- /dev/null +++ b/apps/portal/jsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["src"] +} diff --git a/apps/portal/package.json b/apps/portal/package.json new file mode 100644 index 0000000..98768e5 --- /dev/null +++ b/apps/portal/package.json @@ -0,0 +1,138 @@ +{ + "name": "@tryghost/portal", + "version": "2.68.10", + "license": "MIT", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "files": [ + "umd/", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "dev": "concurrently \"pnpm preview -l silent\" \"pnpm build:watch\"", + "build": "vite build", + "build:watch": "vite build --watch", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:ci": "pnpm test --coverage", + "test:unit": "pnpm test:ci", + "lint:code": "eslint src test --ext .js,.ts --cache", + "lint:types": "tsc --noEmit", + "lint": "pnpm lint:code && pnpm lint:types", + "preship": "pnpm lint", + "ship": "node ../../.github/scripts/release-apps.js", + "prepublishOnly": "pnpm build" + }, + "eslintConfig": { + "env": { + "browser": true + }, + "globals": { + "vi": "readonly", + "describe": "readonly", + "it": "readonly", + "test": "readonly", + "expect": "readonly", + "beforeEach": "readonly", + "afterEach": "readonly", + "beforeAll": "readonly", + "afterAll": "readonly", + "require": "readonly" + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2022 + }, + "extends": [ + "plugin:ghost/browser", + "plugin:i18next/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime" + ], + "plugins": [ + "ghost", + "i18next" + ], + "rules": { + "react/prop-types": "off", + "ghost/filenames/match-regex": [ + "error", + "^[a-z0-9.-]+$", + false + ] + }, + "settings": { + "react": { + "version": "detect" + } + }, + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2022, + "ecmaFeatures": { + "jsx": true + }, + "project": "./tsconfig.json" + }, + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "plugins": [ + "@typescript-eslint" + ] + } + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/eslint-parser": "7.28.4", + "@doist/react-interpolate": "2.2.1", + "@sentry/react": "7.120.4", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "12.1.5", + "@testing-library/user-event": "14.6.1", + "@tryghost/i18n": "workspace:*", + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "3.2.4", + "@vitest/ui": "3.2.4", + "concurrently": "8.2.2", + "cross-fetch": "4.1.0", + "dompurify": "3.3.1", + "eslint": "catalog:", + "eslint-plugin-i18next": "6.1.3", + "jsdom": "28.1.0", + "react": "17.0.2", + "react-dom": "17.0.2", + "vite": "5.4.21", + "vite-plugin-css-injected-by-js": "3.5.2", + "vite-plugin-svgr": "3.3.0", + "vitest": "3.2.4" + }, + "dependencies": { + "@tryghost/debug": "0.1.40" + } +} diff --git a/apps/portal/src/actions.js b/apps/portal/src/actions.js new file mode 100644 index 0000000..662afa6 --- /dev/null +++ b/apps/portal/src/actions.js @@ -0,0 +1,803 @@ +import setupGhostApi from './utils/api'; +import {chooseBestErrorMessage} from './utils/errors'; +import {createNotification, createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl, getRefDomain} from './utils/helpers'; +import {t} from './utils/i18n'; + +function switchPage({data, state}) { + return { + page: data.page, + popupNotification: null, + lastPage: data.lastPage || null, + pageData: data.pageData || state.pageData + }; +} + +function togglePopup({state}) { + return { + showPopup: !state.showPopup + }; +} + +function openPopup({data}) { + return { + showPopup: true, + reloadOnPopupClose: false, + page: data.page, + ...(data.pageQuery ? {pageQuery: data.pageQuery} : {}), + ...(data.pageData ? {pageData: data.pageData} : {}) + }; +} + +function back({state}) { + if (state.lastPage) { + return { + page: state.lastPage + }; + } else { + return closePopup({state}); + } +} + +function closePopup({state}) { + removePortalLinkFromUrl(); + return { + showPopup: false, + lastPage: null, + pageQuery: '', + popupNotification: null, + page: state.page === 'magiclink' ? '' : state.page + }; +} + +function openNotification({data, state}) { + const { + action = 'openNotification', + status = 'success', + autoHide = true, + closeable = true, + duration = 2600, + message = '' + } = data || {}; + + const notification = createNotification({ + type: action, + status, + autoHide, + closeable, + duration, + state, + message + }); + + return { + notification, + notificationSequence: notification.count + }; +} + +function closeNotification() { + return { + notification: null + }; +} + +async function signout({api, state}) { + try { + await api.member.signout(); + return { + action: 'signout:success' + }; + } catch (e) { + return { + action: 'signout:failed', + popupNotification: createPopupNotification({ + type: 'signout:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to log out, please try again') + }) + }; + } +} + +async function signin({data, api, state}) { + try { + const integrityToken = await api.member.getIntegrityToken(); + const payload = { + ...data, + emailType: 'signin', + integrityToken, + includeOTC: true + }; + const {otc_ref: otcRef, inboxLinks} = await api.member.sendMagicLink(payload); + return { + page: 'magiclink', + lastPage: 'signin', + ...(otcRef ? {otcRef} : {}), + inboxLinks, + pageData: { + ...(state.pageData || {}), + email: (data?.email || '').trim() + } + }; + } catch (e) { + return { + action: 'signin:failed', + popupNotification: createPopupNotification({ + type: 'signin:failed', autoHide: false, closeable: true, state, status: 'error', + message: chooseBestErrorMessage(e, t('Failed to log in, please try again')) + }) + }; + } +} + +function startSigninOTCFromCustomForm({data, state}) { + const email = (data?.email || '').trim(); + const otcRef = data?.otcRef; + const inboxLinks = data?.inboxLinks; + + if (!otcRef) { + return {}; + } + + return { + showPopup: true, + page: 'magiclink', + lastPage: 'signin', + otcRef, + inboxLinks, + pageData: { + ...(state.pageData || {}), + email + }, + popupNotification: null + }; +} + +async function verifyOTC({data, api}) { + const genericErrorMessage = t('Failed to verify code, please try again'); + + try { + const integrityToken = await api.member.getIntegrityToken(); + const response = await api.member.verifyOTC({...data, integrityToken}); + + if (response.redirectUrl) { + return window.location.assign(response.redirectUrl); + } else { + return { + action: 'verifyOTC:failed', + actionErrorMessage: chooseBestErrorMessage(response.errors?.[0], genericErrorMessage) + }; + } + } catch (e) { + return { + action: 'verifyOTC:failed', + actionErrorMessage: chooseBestErrorMessage(e, genericErrorMessage) + }; + } +} + +async function signup({data, state, api}) { + try { + let {plan, tierId, cadence, email, name, newsletters, offerId} = data; + name = name?.trim(); + + let inboxLinks; + if (plan.toLowerCase() === 'free') { + const integrityToken = await api.member.getIntegrityToken(); + ({inboxLinks} = await api.member.sendMagicLink({emailType: 'signup', integrityToken, ...data, name})); + } else { + if (tierId && cadence) { + await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId}); + } else { + ({tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan})); + await api.member.checkoutPlan({plan, tierId, cadence, email, name, newsletters, offerId}); + } + return { + page: 'loading' + }; + } + return { + page: 'magiclink', + lastPage: 'signup', + inboxLinks, + pageData: { + ...(state.pageData || {}), + email: (email || '').trim() + } + }; + } catch (e) { + const message = chooseBestErrorMessage(e, t('Failed to sign up, please try again')); + return { + action: 'signup:failed', + popupNotification: createPopupNotification({ + type: 'signup:failed', autoHide: false, closeable: true, state, status: 'error', + message: message + }) + }; + } +} + +async function redeemGift({data, state, api}) { + try { + let {email, name, giftToken} = data; + name = name?.trim(); + + if (state.member) { + await api.gift.redeem({token: giftToken}); + const member = await api.member.sessionData(); + const notification = createNotification({ + type: 'giftRedeem', + status: 'success', + autoHide: true, + closeable: true, + state + }); + + return { + action: 'redeemGift:success', + member, + page: 'accountHome', + notification, + notificationSequence: notification.count + }; + } + + const integrityToken = await api.member.getIntegrityToken(); + const redirectUrl = new URL(state?.site?.url || window.location.href); + const hashParams = new URLSearchParams({ + giftRedemption: 'true' + }); + redirectUrl.hash = `/portal/account?${hashParams.toString()}`; + + const {otc_ref: otcRef, inboxLinks} = await api.member.sendMagicLink({ + email: (email || '').trim(), + emailType: 'subscribe', + integrityToken, + includeOTC: true, + redirect: redirectUrl.href, + giftToken, + ...(name ? {name} : {}) + }); + + return { + page: 'magiclink', + lastPage: 'giftRedemption', + ...(otcRef ? {otcRef} : {}), + inboxLinks, + pageData: { + ...(state.pageData || {}), + email: (email || '').trim(), + redirect: redirectUrl.href + } + }; + } catch (e) { + return { + action: 'redeemGift:failed', + popupNotification: createPopupNotification({ + type: 'redeemGift:failed', + autoHide: false, + closeable: true, + state, + status: 'error', + message: chooseBestErrorMessage(e, 'Failed to redeem gift, please try again') // TODO: Add translation strings once copy has been finalised + }) + }; + } +} + +async function checkoutPlan({data, state, api}) { + try { + let {plan, offerId, tierId, cadence} = data; + if (!tierId || !cadence) { + ({tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: plan})); + } + await api.member.checkoutPlan({ + plan, + tierId, + cadence, + offerId, + metadata: { + checkoutType: 'upgrade' + } + }); + } catch (e) { + return { + action: 'checkoutPlan:failed', + popupNotification: createPopupNotification({ + type: 'checkoutPlan:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to process checkout, please try again') + }) + }; + } +} + +async function checkoutGift({data, state, api}) { + try { + const {tierId, cadence} = data; + await api.member.checkoutGift({tierId, cadence}); + return { + action: 'checkoutGift:success' + }; + } catch (e) { + return { + action: 'checkoutGift:failed', + popupNotification: createPopupNotification({ + type: 'checkoutGift:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to process checkout, please try again') + }) + }; + } +} + +async function updateSubscription({data, state, api}) { + try { + const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data; + const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: planId}); + + await api.member.updateSubscription({ + planName: plan, + tierId, + cadence, + subscriptionId, + cancelAtPeriodEnd, + planId: planId + }); + const member = await api.member.sessionData(); + const action = 'updateSubscription:success'; + return { + action, + popupNotification: createPopupNotification({ + type: action, autoHide: true, closeable: true, state, status: 'success', + message: t('Subscription plan updated successfully') + }), + page: 'accountHome', + member: member + }; + } catch (e) { + return { + action: 'updateSubscription:failed', + popupNotification: createPopupNotification({ + type: 'updateSubscription:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to update subscription, please try again') + }) + }; + } +} + +async function cancelSubscription({data, state, api}) { + try { + const {subscriptionId, cancellationReason} = data; + await api.member.updateSubscription({ + subscriptionId, smartCancel: true, cancellationReason + }); + const member = await api.member.sessionData(); + const action = 'cancelSubscription:success'; + return { + action, + page: 'accountHome', + member: member, + reloadOnPopupClose: true + }; + } catch (e) { + return { + action: 'cancelSubscription:failed', + popupNotification: createPopupNotification({ + type: 'cancelSubscription:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to cancel subscription, please try again') + }) + }; + } +} + +async function continueSubscription({data, state, api}) { + try { + const {subscriptionId} = data; + await api.member.updateSubscription({ + subscriptionId, cancelAtPeriodEnd: false + }); + const member = await api.member.sessionData(); + const action = 'continueSubscription:success'; + return { + action, + page: 'accountHome', + member: member, + reloadOnPopupClose: true + }; + } catch (e) { + return { + action: 'continueSubscription:failed', + popupNotification: createPopupNotification({ + type: 'continueSubscription:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to cancel subscription, please try again') + }) + }; + } +} + +async function applyOffer({data, state, api}) { + try { + const {offerId, subscriptionId} = data; + await api.member.applyOffer({ + offerId, + subscriptionId + }); + const member = await api.member.sessionData(); + const action = 'applyOffer:success'; + return { + action, + page: 'accountHome', + member: member, + offers: [], + reloadOnPopupClose: true, + popupNotification: createPopupNotification({ + type: 'applyOffer:success', autoHide: true, closeable: true, state, status: 'success', + message: 'Offer applied successfully!' + }) + }; + } catch (e) { + return { + action: 'applyOffer:failed', + popupNotification: createPopupNotification({ + type: 'applyOffer:failed', autoHide: false, closeable: true, state, status: 'error', + message: 'Failed to apply offer, please try again' + }) + }; + } +} + +async function editBilling({data, state, api}) { + try { + await api.member.editBilling(data); + } catch (e) { + return { + action: 'editBilling:failed', + popupNotification: createPopupNotification({ + type: 'editBilling:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to update billing information, please try again') + }) + }; + } +} + +async function manageBilling({data, state, api}) { + try { + await api.member.manageBilling(data); + } catch (e) { + return { + action: 'manageBilling:failed', + popupNotification: createPopupNotification({ + type: 'manageBilling:failed', autoHide: false, closeable: true, state, status: 'error', + message: t('Failed to open billing portal, please try again') + }) + }; + } +} + +async function clearPopupNotification() { + return { + popupNotification: null + }; +} + +async function showPopupNotification({data, state}) { + let {action, message = ''} = data; + action = action || 'showPopupNotification:success'; + return { + popupNotification: createPopupNotification({ + type: action, + autoHide: true, + closeable: true, + state, + status: 'success', + message + }) + }; +} + +async function updateNewsletterPreference({data, state, api}) { + try { + const {newsletters, enableCommentNotifications} = data; + if (!newsletters && enableCommentNotifications === undefined) { + return {}; + } + const updateData = {}; + if (newsletters) { + updateData.newsletters = newsletters; + } + if (enableCommentNotifications !== undefined) { + updateData.enableCommentNotifications = enableCommentNotifications; + } + const member = await api.member.update(updateData); + const action = 'updateNewsletterPref:success'; + return { + action, + member + }; + } catch (e) { + return { + action: 'updateNewsletterPref:failed', + popupNotification: createPopupNotification({ + type: 'updateNewsletter:failed', + autoHide: true, closeable: true, state, status: 'error', + message: t('Failed to update newsletter settings') + }) + }; + } +} + +async function removeEmailFromSuppressionList({state, api}) { + try { + await api.member.deleteSuppression(); + const action = 'removeEmailFromSuppressionList:success'; + return { + action, + popupNotification: createPopupNotification({ + type: 'removeEmailFromSuppressionList:success', autoHide: true, closeable: true, state, status: 'success', + message: t('You have been successfully resubscribed') + }) + }; + } catch (e) { + return { + action: 'removeEmailFromSuppressionList:failed', + popupNotification: createPopupNotification({ + type: 'removeEmailFromSuppressionList:failed', + autoHide: true, closeable: true, state, status: 'error', + message: t('Your email has failed to resubscribe, please try again') + }) + }; + } +} + +async function updateNewsletter({data, state, api}) { + try { + const {subscribed} = data; + const member = await api.member.update({subscribed}); + if (!member) { + throw new Error('Failed to update newsletter'); + } + const action = 'updateNewsletter:success'; + return { + action, + member: member, + popupNotification: createPopupNotification({ + type: action, autoHide: true, closeable: true, state, status: 'success', + message: t('Email newsletter settings updated') + }) + }; + } catch (e) { + return { + action: 'updateNewsletter:failed', + popupNotification: createPopupNotification({ + type: 'updateNewsletter:failed', autoHide: true, closeable: true, state, status: 'error', + message: t('Failed to update newsletter settings') + }) + }; + } +} + +async function updateMemberEmail({data, state, api}) { + const {email} = data; + const originalEmail = getMemberEmail({member: state.member}); + if (email !== originalEmail) { + try { + await api.member.updateEmailAddress({email}); + return { + success: true + }; + } catch (err) { + return { + success: false, + error: err + }; + } + } + return null; +} + +async function updateMemberData({data, state, api}) { + const name = data?.name?.trim(); + const originalName = getMemberName({member: state.member}); + + if (originalName !== name) { + try { + const member = await api.member.update({name}); + if (!member) { + throw new Error('Failed to update member'); + } + return { + member, + success: true + }; + } catch (err) { + return { + success: false, + error: err + }; + } + } + return null; +} + +async function refreshMemberData({state, api}) { + if (state.member) { + try { + const member = await api.member.sessionData(); + if (member) { + return { + member, + success: true, + action: 'refreshMemberData:success' + }; + } + return null; + } catch (err) { + return { + success: false, + error: err, + action: 'refreshMemberData:failed' + }; + } + } + return null; +} + +async function updateProfile({data, state, api}) { + const [dataUpdate, emailUpdate] = await Promise.all([updateMemberData({data, state, api}), updateMemberEmail({data, state, api})]); + if (dataUpdate && emailUpdate) { + if (emailUpdate.success) { + return { + action: 'updateProfile:success', + ...(dataUpdate.success ? {member: dataUpdate.member} : {}), + page: 'accountHome', + popupNotification: createPopupNotification({ + type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state, + message: t('Check your inbox to verify email update') + }) + }; + } + + const message = !dataUpdate.success ? t('Failed to update account data') : t('Failed to send verification email'); + return { + action: 'updateProfile:failed', + ...(dataUpdate.success ? {member: dataUpdate.member} : {}), + popupNotification: createPopupNotification({ + type: 'updateProfile:failed', autoHide: true, closeable: true, status: 'error', message, state + }) + }; + } else if (dataUpdate) { + const action = dataUpdate.success ? 'updateProfile:success' : 'updateProfile:failed'; + const status = dataUpdate.success ? 'success' : 'error'; + const message = !dataUpdate.success ? t('Failed to update account details') : t('Account details updated successfully'); + return { + action, + ...(dataUpdate.success ? {member: dataUpdate.member} : {}), + ...(dataUpdate.success ? {page: 'accountHome'} : {}), + popupNotification: createPopupNotification({ + type: action, autoHide: dataUpdate.success, closeable: true, status, state, message + }) + }; + } else if (emailUpdate) { + const action = emailUpdate.success ? 'updateProfile:success' : 'updateProfile:failed'; + const status = emailUpdate.success ? 'success' : 'error'; + let message = ''; + + if (emailUpdate.error) { + message = chooseBestErrorMessage(emailUpdate.error, t('Failed to send verification email')); + } else { + message = t('Check your inbox to verify email update'); + } + + return { + action, + ...(emailUpdate.success ? {page: 'accountHome'} : {}), + popupNotification: createPopupNotification({ + type: action, autoHide: emailUpdate.success, closeable: true, status, state, message + }) + }; + } + return { + action: 'updateProfile:success', + page: 'accountHome', + popupNotification: createPopupNotification({ + type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state, + message: t('Account details updated successfully') + }) + }; +} + +async function oneClickSubscribe({data: {siteUrl}, state}) { + const externalSiteApi = setupGhostApi({siteUrl: siteUrl, apiUrl: 'not-defined', contentApiKey: 'not-defined'}); + const {member} = state; + + const referrerUrl = window.location.href; + const referrerSource = getRefDomain(); + + const integrityToken = await externalSiteApi.member.getIntegrityToken(); + await externalSiteApi.member.sendMagicLink({ + emailType: 'signup', + name: member.name, + email: member.email, + autoRedirect: false, + integrityToken, + customUrlHistory: state.site.outbound_link_tagging ? [ + { + time: Date.now(), + referrerSource, + referrerMedium: 'Ghost Recommendations', + referrerUrl + } + ] : [] + }); + + return {}; +} + +function trackRecommendationClicked({data: {recommendationId}, api}) { + try { + const existing = localStorage.getItem('ghost-recommendations-clicked'); + const clicked = existing ? JSON.parse(existing) : []; + if (clicked.includes(recommendationId)) { + // Already tracked + return; + } + clicked.push(recommendationId); + localStorage.setItem('ghost-recommendations-clicked', JSON.stringify(clicked)); + } catch (e) { + // Ignore localstorage errors (browser not supported or in private mode) + } + api.recommendations.trackClicked({ + recommendationId + }); + + return {}; +} + +async function trackRecommendationSubscribed({data: {recommendationId}, api}) { + api.recommendations.trackSubscribed({ + recommendationId + }); + + return {}; +} + +const Actions = { + togglePopup, + openPopup, + closePopup, + switchPage, + openNotification, + closeNotification, + back, + signout, + signin, + startSigninOTCFromCustomForm, + verifyOTC, + signup, + redeemGift, + updateSubscription, + cancelSubscription, + continueSubscription, + applyOffer, + updateNewsletter, + updateProfile, + refreshMemberData, + clearPopupNotification, + editBilling, + manageBilling, + checkoutPlan, + checkoutGift, + updateNewsletterPreference, + showPopupNotification, + removeEmailFromSuppressionList, + oneClickSubscribe, + trackRecommendationClicked, + trackRecommendationSubscribed +}; + +/** Handle actions in the App, returns updated state */ +export default async function ActionHandler({action, data, state, api}) { + const handler = Actions[action]; + if (handler) { + return await handler({data, state, api}) || {}; + } + return {}; +} diff --git a/apps/portal/src/app-context.js b/apps/portal/src/app-context.js new file mode 100644 index 0000000..da6d628 --- /dev/null +++ b/apps/portal/src/app-context.js @@ -0,0 +1,20 @@ +// Ref: https://reactjs.org/docs/context.html +import React from 'react'; + +const AppContext = React.createContext({ + site: {}, + member: {}, + offers: [], + action: '', + actionErrorMessage: null, + lastPage: '', + brandColor: '', + pageData: {}, + doAction: (action, data) => { + return {action, data}; + }, + dir: 'ltr' + +}); + +export default AppContext; diff --git a/apps/portal/src/app.css b/apps/portal/src/app.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/portal/src/app.js b/apps/portal/src/app.js new file mode 100644 index 0000000..9abeed5 --- /dev/null +++ b/apps/portal/src/app.js @@ -0,0 +1,1320 @@ +import React from 'react'; +import * as Sentry from '@sentry/react'; +import i18n, {t} from './utils/i18n'; +import {chooseBestErrorMessage} from './utils/errors'; +import TriggerButton from './components/trigger-button'; +import Notification from './components/notification'; +import PopupModal from './components/popup-modal'; +import setupGhostApi from './utils/api'; +import AppContext from './app-context'; +import NotificationParser, {clearURLParams} from './utils/notifications'; +import * as Fixtures from './utils/fixtures'; +import {hasMode} from './utils/check-mode'; +import {transformPortalAnchorToRelative} from './utils/transform-portal-anchor-to-relative'; +import {getActivePage, isAccountPage, isOfferPage} from './pages'; +import ActionHandler from './actions'; +import {getGiftRedemptionErrorMessage} from './utils/gift-redemption-notification'; +import './app.css'; +import {hasRecommendations, hasGiftSubscriptions, createNotification, createPopupNotification, hasAvailablePrices, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isRetentionOffer, isComplimentaryMember, isInviteOnly, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers'; +import {validateHexColor} from './utils/sanitize-html'; +import {handleDataAttributes} from './data-attributes'; + +const safeDecodeURIComponent = (value) => { + try { + return decodeURIComponent(value); + } catch (error) { + return null; + } +}; + +const staleGiftRedemptionRequestResult = { + staleGiftRedemptionRequest: true +}; + +const DEV_MODE_DATA = { + showPopup: true, + site: Fixtures.site, + member: Fixtures.member.free, + page: 'accountEmail', + ...Fixtures.paidMemberOnTier(), + pageData: Fixtures.offer +}; + +function SentryErrorBoundary({site, children}) { + const {portal_sentry: portalSentry} = site || {}; + if (portalSentry && portalSentry.dsn) { + return ( + + {children} + + ); + } + return ( + <> + {children} + + ); +} + +export default class App extends React.Component { + constructor(props) { + super(props); + + this.setupCustomTriggerButton(); + + this.state = { + site: null, + member: null, + offers: [], + page: 'loading', + showPopup: false, + action: 'init:running', + actionErrorMessage: null, + initStatus: 'running', + lastPage: null, + notification: null, + notificationSequence: -1, + customSiteUrl: props.customSiteUrl, + locale: props.locale, + scrollbarWidth: 0 + }; + + this._redemptionRequestId = 0; + this.currentRedemptionToken = null; + } + + componentDidMount() { + const scrollbarWidth = this.getScrollbarWidth(); + this.setState({scrollbarWidth}); + + this.initSetup(); + } + + componentDidUpdate(prevProps, prevState) { + /**Handle custom trigger class change on popup open state change */ + if (prevState.showPopup !== this.state.showPopup) { + this.handleCustomTriggerClassUpdate(); + + /** Remove background scroll when popup is opened */ + try { + if (this.state.showPopup) { + /** When modal is opened, store current overflow and set as hidden */ + this.bodyScroll = window.document?.body?.style?.overflow; + this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right'); + window.document.body.style.overflow = 'hidden'; + if (this.state.scrollbarWidth) { + window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`; + } + } else { + /** When the modal is hidden, reset overflow property for body */ + window.document.body.style.overflow = this.bodyScroll || ''; + if (!this.bodyMargin || this.bodyMargin === '0px') { + window.document.body.style.marginRight = ''; + } else { + window.document.body.style.marginRight = this.bodyMargin; + } + if (this.state.reloadOnPopupClose) { + window.location.reload(); + } + } + } catch (e) { + /** Ignore any errors for scroll handling */ + } + } + + if (this.state.initStatus === 'success' && prevState.initStatus !== this.state.initStatus) { + const {siteUrl} = this.props; + const contextState = this.getContextFromState(); + this.sendPortalReadyEvent(); + handleDataAttributes({ + siteUrl, + site: contextState.site, + member: contextState.member, + offers: contextState.offers, + doAction: contextState.doAction, + captureException: Sentry.captureException + }); + } + } + + componentWillUnmount() { + /**Clear timeouts and event listeners on unmount */ + clearTimeout(this.timeoutId); + this.customTriggerButtons && this.customTriggerButtons.forEach((customTriggerButton) => { + customTriggerButton.removeEventListener('click', this.clickHandler); + }); + window.removeEventListener('hashchange', this.hashHandler, false); + } + + sendPortalReadyEvent() { + if (window.self !== window.parent) { + window.parent.postMessage({ + type: 'portal-ready', + payload: {} + }, '*'); + } + } + + // User for adding trailing margin to prevent layout shift when popup appears + getScrollbarWidth() { + // Create a temporary div + const div = document.createElement('div'); + div.style.visibility = 'hidden'; + div.style.overflow = 'scroll'; // forcing scrollbar to appear + document.body.appendChild(div); + + // Create an inner div + // const inner = document.createElement('div'); + document.body.appendChild(div); + + // Calculate the width difference + const scrollbarWidth = div.offsetWidth - div.clientWidth; + + // Clean up + document.body.removeChild(div); + + return scrollbarWidth; + } + + /** Setup custom trigger buttons handling on page */ + setupCustomTriggerButton() { + // Handler for custom buttons + this.clickHandler = async (event) => { + event.preventDefault(); + const target = event.currentTarget; + const pagePath = (target && target.dataset.portal); + const linkData = this.getPageFromLinkPath(pagePath); + if (!linkData) { + return; + } + const {page, pageQuery, pageData} = linkData; + if (this.state.initStatus === 'success') { + if (page === 'gift' && !hasGiftSubscriptions({site: this.state.site})) { + this.invalidateGiftRedemptionRequest(); + removePortalLinkFromUrl(); + + return; + } + if (page === 'giftRedemption' && pageData?.token) { + const redemptionRequest = this.startGiftRedemptionRequest(pageData.token); + const giftLinkData = await this.fetchGiftRedemptionData({ + site: this.state.site, + token: pageData.token + }); + + if (!this.isCurrentGiftRedemptionRequest(redemptionRequest)) { + return; + } + + this.setState(giftLinkData); + return; + } + + this.invalidateGiftRedemptionRequest(); + if (pageQuery && pageQuery !== 'free') { + this.handleSignupQuery({site: this.state.site, pageQuery}); + } else { + this.dispatchAction('openPopup', {page, pageQuery, pageData}); + } + } + }; + const customTriggerSelector = '[data-portal]'; + const popupCloseClass = 'gh-portal-close'; + this.customTriggerButtons = document.querySelectorAll(customTriggerSelector) || []; + this.customTriggerButtons.forEach((customTriggerButton) => { + customTriggerButton.classList.add(popupCloseClass); + // Remove any existing event listener + customTriggerButton.removeEventListener('click', this.clickHandler); + customTriggerButton.addEventListener('click', this.clickHandler); + }); + } + + /** Handle portal class set on custom trigger buttons */ + handleCustomTriggerClassUpdate() { + const popupOpenClass = 'gh-portal-open'; + const popupCloseClass = 'gh-portal-close'; + this.customTriggerButtons?.forEach((customButton) => { + const elAddClass = this.state.showPopup ? popupOpenClass : popupCloseClass; + const elRemoveClass = this.state.showPopup ? popupCloseClass : popupOpenClass; + customButton.classList.add(elAddClass); + customButton.classList.remove(elRemoveClass); + }); + } + + startGiftRedemptionRequest(token) { + this._redemptionRequestId += 1; + this.currentRedemptionToken = token; + + return { + requestId: this._redemptionRequestId, + token + }; + } + + invalidateGiftRedemptionRequest() { + this._redemptionRequestId += 1; + this.currentRedemptionToken = null; + } + + isCurrentGiftRedemptionRequest({requestId, token}) { + return this._redemptionRequestId === requestId && this.currentRedemptionToken === token; + } + + /** Initialize portal setup on load, fetch data and setup state*/ + async initSetup() { + try { + // Fetch data from API, links, preview, dev sources + const {site, member, offers, page, showPopup, popupNotification, notification, notificationSequence, lastPage, pageQuery, pageData} = await this.fetchData(); + const i18nLanguage = this.props.siteI18nEnabled ? this.props.locale || site.locale || 'en' : 'en'; + i18n.changeLanguage(i18nLanguage); + + const state = { + site, + member, + offers, + page, + lastPage, + pageQuery, + showPopup, + pageData, + popupNotification, + notification, + notificationSequence, + dir: i18n.dir() || 'ltr', + action: 'init:success', + initStatus: 'success', + locale: i18nLanguage + }; + + this.handleSignupQuery({site, pageQuery, member}); + + this.setState(state); + + // Listen to preview mode changes + this.hashHandler = () => { + this.updateStateForPreviewLinks(); + }; + window.addEventListener('hashchange', this.hashHandler, false); + + // the signup card will ship hidden by default, + // so we need to show it if the member is not logged in + if (!member) { + const formElements = document.querySelectorAll('[data-lexical-signup-form]'); + if (formElements.length > 0){ + formElements.forEach((element) => { + element.style.display = ''; + }); + } + } + + this.setupRecommendationButtons(); + + // avoid portal links switching to homepage (e.g. from absolute link copy/pasted from Admin) + this.transformPortalLinksToRelative(); + } catch (e) { + /* eslint-disable no-console */ + console.error(`[Portal] Failed to initialize:`, e); + /* eslint-enable no-console */ + this.setState({ + action: 'init:failed', + initStatus: 'failed' + }); + } + } + + /** Fetch state data from all available sources */ + async fetchData() { + const {site: apiSiteData, member, offers} = await this.fetchApiData(); + const {site: devSiteData, ...restDevData} = this.fetchDevData(); + const linkData = await this.fetchLinkData(apiSiteData, member); + const {site: linkSiteData, ...restLinkData} = linkData?.staleGiftRedemptionRequest ? {} : linkData; + const {site: previewSiteData, ...restPreviewData} = this.fetchPreviewData(); + const {site: notificationSiteData, ...restNotificationData} = this.fetchNotificationData(); + let page = ''; + return { + member, + offers, + page, + site: { + ...apiSiteData, + ...linkSiteData, + ...previewSiteData, + ...notificationSiteData, + ...devSiteData, + plans: { + ...(devSiteData || {}).plans, + ...(apiSiteData || {}).plans, + ...(previewSiteData || {}).plans + } + }, + ...restDevData, + ...restLinkData, + ...restNotificationData, + ...restPreviewData + }; + } + + /** Fetch state for Dev mode */ + fetchDevData() { + // Setup custom dev mode data from fixtures + if (hasMode(['dev']) && !this.state.customSiteUrl) { + return DEV_MODE_DATA; + } + + // Setup test mode data + if (hasMode(['test'])) { + return { + showPopup: this.props.showPopup !== undefined ? this.props.showPopup : true + }; + } + return {}; + } + + /**Fetch state from Offer Preview mode query string*/ + fetchOfferQueryStrData(qs = '') { + const qsParams = new URLSearchParams(qs); + const data = {}; + // Handle the query params key/value pairs + for (let pair of qsParams.entries()) { + const key = pair[0]; + const value = decodeURIComponent(pair[1]); + if (key === 'name') { + data.name = value || ''; + } else if (key === 'code') { + data.code = value || ''; + } else if (key === 'display_title') { + data.display_title = value || ''; + } else if (key === 'display_description') { + data.display_description = value || ''; + } else if (key === 'type') { + data.type = value || ''; + } else if (key === 'cadence') { + data.cadence = value || ''; + } else if (key === 'duration') { + data.duration = value || ''; + } else if (key === 'duration_in_months' && !isNaN(Number(value))) { + data.duration_in_months = Number(value); + } else if (key === 'amount' && !isNaN(Number(value))) { + data.amount = Number(value); + } else if (key === 'currency') { + data.currency = value || ''; + } else if (key === 'status') { + data.status = value || ''; + } else if (key === 'tier_id') { + data.tier = { + id: value || Fixtures.offer.tier.id + }; + } else if (key === 'redemption_type') { + data.redemption_type = value || 'signup'; + } + } + + if (data.redemption_type === 'retention') { + const previewSubscriptionId = Fixtures.member.preview?.subscriptions?.[0]?.id; + + return { + page: 'accountPlan', + offers: [data], + pageData: { + action: 'cancel', + subscriptionId: previewSubscriptionId + } + }; + } + + return { + page: 'offer', + pageData: data + }; + } + + /** Fetch state from Preview mode Query String */ + fetchQueryStrData(qs = '') { + const qsParams = new URLSearchParams(qs); + const data = { + site: { + plans: {} + } + }; + + const allowedPlans = []; + let portalPrices; + let portalProducts = null; + let monthlyPrice, yearlyPrice, currency; + // Handle the query params key/value pairs + for (let pair of qsParams.entries()) { + const key = pair[0]; + + // Note: this needs to be cleaned up, there is no reason why we need to double encode/decode + const value = decodeURIComponent(pair[1]); + + if (key === 'button') { + data.site.portal_button = JSON.parse(value); + } else if (key === 'name') { + data.site.portal_name = JSON.parse(value); + } else if (key === 'isFree' && JSON.parse(value)) { + allowedPlans.push('free'); + } else if (key === 'isMonthly' && JSON.parse(value)) { + allowedPlans.push('monthly'); + } else if (key === 'isYearly' && JSON.parse(value)) { + allowedPlans.push('yearly'); + } else if (key === 'portalPrices') { + portalPrices = value ? value.split(',') : []; + } else if (key === 'portalProducts') { + portalProducts = value ? value.split(',') : []; + } else if (key === 'page' && value) { + data.page = value; + } else if (key === 'accentColor' && (value === '' || value)) { + data.site.accent_color = value; + } else if (key === 'buttonIcon' && value) { + data.site.portal_button_icon = value; + } else if (key === 'signupButtonText') { + data.site.portal_button_signup_text = value || ''; + } else if (key === 'signupTermsHtml') { + data.site.portal_signup_terms_html = value || ''; + } else if (key === 'signupCheckboxRequired') { + data.site.portal_signup_checkbox_required = JSON.parse(value); + } else if (key === 'buttonStyle' && value) { + data.site.portal_button_style = value; + } else if (key === 'monthlyPrice' && !isNaN(Number(value))) { + data.site.plans.monthly = Number(value); + monthlyPrice = Number(value); + } else if (key === 'yearlyPrice' && !isNaN(Number(value))) { + data.site.plans.yearly = Number(value); + yearlyPrice = Number(value); + } else if (key === 'currency' && value) { + const currencyValue = value.toUpperCase(); + data.site.plans.currency = currencyValue; + data.site.plans.currency_symbol = getCurrencySymbol(currencyValue); + currency = currencyValue; + } else if (key === 'disableBackground') { + data.site.disableBackground = JSON.parse(value); + } else if (key === 'membersSignupAccess' && value) { + data.site.members_signup_access = value; + } else if (key === 'portalDefaultPlan' && value) { + data.site.portal_default_plan = value; + } else if (key === 'transistorPortalSettings' && value) { + data.site.transistor_portal_settings = JSON.parse(value); + } + } + data.site.portal_plans = allowedPlans; + data.site.portal_products = portalProducts; + if (portalPrices) { + data.site.portal_plans = portalPrices; + } else if (monthlyPrice && yearlyPrice && currency) { + data.site.prices = [ + { + id: 'monthly', + stripe_price_id: 'dummy_stripe_monthly', + stripe_product_id: 'dummy_stripe_product', + active: 1, + nickname: 'Monthly', + currency: currency, + amount: monthlyPrice, + type: 'recurring', + interval: 'month' + }, + { + id: 'yearly', + stripe_price_id: 'dummy_stripe_yearly', + stripe_product_id: 'dummy_stripe_product', + active: 1, + nickname: 'Yearly', + currency: currency, + amount: yearlyPrice, + type: 'recurring', + interval: 'year' + } + ]; + } + + return data; + } + + /**Fetch state data for billing notification */ + fetchNotificationData() { + const {type, status, duration, autoHide, closeable} = NotificationParser({billingOnly: true}) || {}; + if (['stripe:billing-update'].includes(type)) { + if (status === 'success') { + const popupNotification = createPopupNotification({ + type, status, duration, closeable, autoHide, state: this.state, + message: status === 'success' ? 'Billing info updated successfully' : '' + }); + return { + showPopup: true, + popupNotification + }; + } + return { + showPopup: true + }; + } + return {}; + } + + /** Fetch state from Portal Links */ + async fetchGiftRedemptionData({site, token}) { + if (!hasGiftSubscriptions({site})) { + removePortalLinkFromUrl(); + + return {}; + } + + try { + const response = await this.GhostApi.gift.fetchRedemptionData({token}); + + return { + showPopup: true, + notification: null, + page: 'giftRedemption', + pageData: { + token, + gift: response?.gifts?.[0] || null + } + }; + } catch (error) { + removePortalLinkFromUrl(); + + const notification = createNotification({ + type: 'giftRedemption:failed', + status: 'error', + autoHide: false, + closeable: true, + state: this.state, + message: getGiftRedemptionErrorMessage(error) + }); + + return { + showPopup: false, + pageData: null, + notification, + notificationSequence: notification.count + }; + } + } + + async fetchLinkData(site, member) { + this.invalidateGiftRedemptionRequest(); + + const qParams = new URLSearchParams(window.location.search); + + if (qParams.get('stripe') === 'gift-purchase-success') { + const token = qParams.get('gift_token'); + clearURLParams(['stripe', 'gift_token']); + if (token) { + return { + showPopup: true, + page: 'giftSuccess', + pageData: { + token + } + }; + } + } + + if (qParams.get('action') === 'unsubscribe') { + // if the user is unsubscribing from a newsletter with an old unsubscribe link that we can't validate, push them to newsletter mgmt where they have to log in + if (qParams.get('key') && qParams.get('uuid')) { + return { + showPopup: true, + page: 'unsubscribe', + pageData: { + uuid: qParams.get('uuid'), + key: qParams.get('key'), + newsletterUuid: qParams.get('newsletter'), + comments: qParams.get('comments') + } + }; + } else { // any malformed unsubscribe links should simply go to email prefs + return { + showPopup: true, + page: 'accountEmail', + pageData: { + newsletterUuid: qParams.get('newsletter'), + action: 'unsubscribe', + redirect: site.url + '#/portal/account/newsletters' + } + }; + } + } + + if (hasRecommendations({site}) && qParams.get('action') === 'signup' && qParams.get('success') === 'true') { + // After a successful signup, we show the recommendations if they are enabled + return { + showPopup: true, + page: 'recommendations', + pageData: { + signup: true + } + }; + } + + const [path, hashQueryString] = window.location.hash.substr(1).split('?'); + const hashQuery = new URLSearchParams(hashQueryString ?? ''); + const productMonthlyPriceQueryRegex = /^(?:(\w+?))?\/monthly$/; + const productYearlyPriceQueryRegex = /^(?:(\w+?))?\/yearly$/; + const offersRegex = /^offers\/(\w+?)\/?$/; + const giftRedemptionRegex = /^\/portal\/gift\/redeem\/([^/?#]+)\/?$/; + const linkRegex = /^\/portal\/?(?:\/(\w+(?:\/\w+)*))?\/?$/; + const shareRegex = /^\/share\/?$/; + const feedbackRegex = /^\/feedback\/(\w+?)\/(\w+?)\/?$/; + + if (path && feedbackRegex.test(path)) { + const [, postId, scoreString] = path.match(feedbackRegex); + const score = parseInt(scoreString); + if (score === 1 || score === 0) { + // if logged in, submit feedback + if (member || (hashQuery.get('uuid') && hashQuery.get('key'))) { + return { + showPopup: true, + page: 'feedback', + pageData: { + uuid: member ? null : hashQuery.get('uuid'), + key: member ? null : hashQuery.get('key'), + postId, + score + } + }; + } else { + return { + showPopup: true, + page: 'signin', + pageData: { + redirect: site.url + `#/feedback/${postId}/${score}/` + } + }; + } + } + } + if (path && giftRedemptionRegex.test(path)) { + const [, token] = path.match(giftRedemptionRegex); + const decodedToken = safeDecodeURIComponent(token); + if (!decodedToken) { + return {}; + } + + const redemptionRequest = this.startGiftRedemptionRequest(decodedToken); + const giftLinkData = await this.fetchGiftRedemptionData({ + site, + token: decodedToken + }); + + if (!this.isCurrentGiftRedemptionRequest(redemptionRequest)) { + return staleGiftRedemptionRequestResult; + } + + return giftLinkData; + } + if (path && shareRegex.test(path)) { + return { + showPopup: true, + page: 'share' + }; + } + + if (path && linkRegex.test(path)) { + const [,pagePath] = path.match(linkRegex); + + const {page, pageQuery, pageData} = this.getPageFromLinkPath(pagePath, site) || {}; + + // If user is not logged in and trying to access an account page, + // redirect to signin with a redirect URL back to the intended page + if (!member && page && isAccountPage({page})) { + return { + showPopup: true, + page: 'signin', + pageData: { + redirect: site.url + `#/portal/${pagePath}/` + } + }; + } + + if (page === 'gift' && !hasGiftSubscriptions({site})) { + removePortalLinkFromUrl(); + + return {}; + } + + const lastPage = ['accountPlan', 'accountProfile'].includes(page) ? 'accountHome' : null; + const showPopup = ( + ['monthly', 'yearly'].includes(pageQuery) || + productMonthlyPriceQueryRegex.test(pageQuery) || + productYearlyPriceQueryRegex.test(pageQuery) || + offersRegex.test(pageQuery) + ) ? false : true; + return { + showPopup, + ...(page ? {page} : {}), + ...(pageQuery ? {pageQuery} : {}), + ...(pageData ? {pageData} : {}), + ...(lastPage ? {lastPage} : {}) + }; + } + return {}; + } + + /** Fetch state from Preview mode */ + fetchPreviewData() { + const [, qs] = window.location.hash.substr(1).split('?'); + if (hasMode(['preview'])) { + let data = {}; + if (hasMode(['offerPreview'])) { + data = this.fetchOfferQueryStrData(qs); + } else { + data = this.fetchQueryStrData(qs); + } + return { + ...data, + showPopup: true + }; + } + return {}; + } + + /* Get the accent color from data attributes */ + getColorOverride() { + const scriptTag = document.querySelector('script[data-ghost]'); + if (scriptTag && scriptTag.dataset.accentColor) { + return scriptTag.dataset.accentColor; + } + return false; + } + + /** Fetch site, member session data and member offers with Ghost Apis */ + async fetchApiData() { + const {siteUrl, customSiteUrl, apiUrl, apiKey} = this.props; + try { + this.GhostApi = this.props.api || setupGhostApi({siteUrl, apiUrl, apiKey}); + const {site, member, offers} = await this.GhostApi.init(); + + const colorOverride = this.getColorOverride(); + if (colorOverride) { + site.accent_color = colorOverride; + } + + this.setupFirstPromoter({site, member}); + this.setupSentry({site}); + return {site, member, offers}; + } catch (e) { + if (hasMode(['dev', 'test'], {customSiteUrl})) { + return {}; + } + + throw e; + } + } + + /** Setup Sentry */ + setupSentry({site}) { + if (hasMode(['test'])) { + return null; + } + const {portal_sentry: portalSentry, portal_version: portalVersion, version: ghostVersion} = site; + // eslint-disable-next-line no-undef + const appVersion = REACT_APP_VERSION || portalVersion; + const releaseTag = `portal@${appVersion}|ghost@${ghostVersion}`; + if (portalSentry && portalSentry.dsn) { + Sentry.init({ + dsn: portalSentry.dsn, + environment: portalSentry.env || 'development', + release: releaseTag, + beforeSend: (event) => { + if (isSentryEventAllowed({event})) { + return event; + } + return null; + }, + allowUrls: [ + /https?:\/\/((www)\.)?unpkg\.com\/@tryghost\/portal/ + ] + }); + } + } + + /** Setup Firstpromoter script */ + setupFirstPromoter({site, member}) { + if (hasMode(['test'])) { + return null; + } + const firstPromoterId = getFirstpromoterId({site}); + let siteDomain = getSiteDomain({site}); + // Replace any leading subdomain and prefix the siteDomain with + // a `.` to allow the FPROM cookie to be accessible across all subdomains + // or the root. + siteDomain = siteDomain?.replace(/^(\S*\.)?(\S*\.\S*)$/i, '.$2'); + + if (firstPromoterId && siteDomain) { + const fpScript = document.createElement('script'); + fpScript.type = 'text/javascript'; + fpScript.async = !0; + fpScript.src = 'https://cdn.firstpromoter.com/fprom.js'; + fpScript.onload = fpScript.onreadystatechange = function () { + let _t = this.readyState; + if (!_t || 'complete' === _t || 'loaded' === _t) { + try { + window.$FPROM.init(firstPromoterId, siteDomain); + if (isRecentMember({member})) { + const email = member.email; + const uid = member.uuid; + if (window.$FPROM) { + window.$FPROM.trackSignup({email: email, uid: uid}); + } else { + const _fprom = window._fprom || []; + window._fprom = _fprom; + _fprom.push(['event', 'signup']); + _fprom.push(['email', email]); + _fprom.push(['uid', uid]); + } + } + } catch (err) { + // Log FP tracking failure + } + } + }; + const firstScript = document.getElementsByTagName('script')[0]; + firstScript.parentNode.insertBefore(fpScript, firstScript); + } + } + + /** Handle actions from across App and update App state */ + async dispatchAction(action, data) { + clearTimeout(this.timeoutId); + this.setState({ + action: `${action}:running`, + actionErrorMessage: null + }); + try { + const updatedState = await ActionHandler({action, data, state: this.state, api: this.GhostApi}); + this.setState(updatedState); + + /** Reset action state after short timeout if not failed*/ + if (updatedState && updatedState.action && !updatedState.action.includes(':failed')) { + this.timeoutId = setTimeout(() => { + this.setState({ + action: '' + }); + }, 2000); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`[Portal] Failed to dispatch action: ${action}`, error); + + if (data && data.throwErrors) { + throw error; + } + + const popupNotification = createPopupNotification({ + type: `${action}:failed`, + autoHide: true, closeable: true, status: 'error', state: this.state, + meta: { + error + } + }); + this.setState({ + action: `${action}:failed`, + actionErrorMessage: chooseBestErrorMessage(error, t('An unexpected error occured. Please try again or contact support if the error persists.')), + popupNotification + }); + } + } + + /**Handle state update for preview url and Portal Link changes */ + async updateStateForPreviewLinks() { + const {site: previewSite, ...restPreviewData} = this.fetchPreviewData(); + const linkData = await this.fetchLinkData(this.state.site, this.state.member); + if (linkData?.staleGiftRedemptionRequest) { + return; + } + + const {site: linkSite, ...restLinkData} = linkData; + + const updatedState = { + site: { + ...this.state.site, + ...(linkSite || {}), + ...(previewSite || {}), + plans: { + ...(this.state.site && this.state.site.plans), + ...(linkSite || {}).plans, + ...(previewSite || {}).plans + } + }, + ...restLinkData, + ...restPreviewData + }; + this.handleSignupQuery({site: updatedState.site, pageQuery: updatedState.pageQuery}); + this.setState(updatedState); + } + + /** Handle Portal offer urls */ + async handleOfferQuery({site, offerId, member = this.state.member}) { + const {portal_button: portalButton} = site; + removePortalLinkFromUrl(); + + if (!isPaidMember({member}) || isComplimentaryMember({member})) { + try { + const offerData = await this.GhostApi.site.offer({offerId}); + const offer = offerData?.offers[0]; + + if (!offer || !offer.tier) { + return; + } + + // Retention offers are only triggered during a member cancellation flow - they cannot be accessed via an offer link + if (isRetentionOffer({offer})) { + return; + } + + if (!isActiveOffer({site, offer})) { + return; + } + + if (!portalButton) { + const product = getProductFromId({site, productId: offer.tier.id}); + const price = offer.cadence === 'month' ? product.monthlyPrice : product.yearlyPrice; + this.dispatchAction('openPopup', { + page: 'loading' + }); + if (member) { + const {tierId, cadence} = getProductCadenceFromPrice({site, priceId: price.id}); + this.dispatchAction('checkoutPlan', {plan: price.id, offerId, tierId, cadence}); + } else { + const {tierId, cadence} = getProductCadenceFromPrice({site, priceId: price.id}); + this.dispatchAction('signup', {plan: price.id, offerId, tierId, cadence}); + } + } else { + this.dispatchAction('openPopup', { + page: 'offer', + pageData: offerData?.offers[0] + }); + } + } catch (e) { + // ignore invalid portal url + } + } + } + + /** Handle direct signup link for a price */ + handleSignupQuery({site, pageQuery, member}) { + const offerQueryRegex = /^offers\/(\w+?)\/?$/; + let priceId = pageQuery; + if (offerQueryRegex.test(pageQuery || '')) { + const [, offerId] = pageQuery.match(offerQueryRegex); + this.handleOfferQuery({site, offerId, member}); + return; + } + if (getPriceIdFromPageQuery({site, pageQuery})) { + priceId = getPriceIdFromPageQuery({site, pageQuery}); + } + const queryPrice = getQueryPrice({site: site, priceId}); + if (pageQuery + && pageQuery !== 'free' + ) { + removePortalLinkFromUrl(); + const plan = queryPrice?.id || priceId; + if (plan !== 'free') { + this.dispatchAction('openPopup', { + page: 'loading' + }); + } + const {tierId, cadence} = getProductCadenceFromPrice({site, priceId: plan}); + this.dispatchAction('signup', {plan, tierId, cadence}); + } + } + + /**Get Portal page from Link/Data-attribute path*/ + getPageFromLinkPath(path) { + const customPricesSignupRegex = /^signup\/?(?:\/(\w+?))?\/?$/; + const customMonthlyProductSignup = /^signup\/?(?:\/(\w+?))\/monthly\/?$/; + const customYearlyProductSignup = /^signup\/?(?:\/(\w+?))\/yearly\/?$/; + const customOfferRegex = /^offers\/(\w+?)\/?$/; + const giftRedemptionRegex = /^gift\/redeem\/([^/?#]+)\/?$/; + + if (path === undefined || path === '') { + return { + page: 'default' + }; + } else if (giftRedemptionRegex.test(path)) { + const [, token] = path.match(giftRedemptionRegex); + const decodedToken = safeDecodeURIComponent(token); + if (!decodedToken) { + return null; + } + + return { + page: 'giftRedemption', + pageData: { + token: decodedToken + } + }; + } else if (customOfferRegex.test(path)) { + return { + pageQuery: path + }; + } else if (path === 'signup') { + return { + page: 'signup' + }; + } else if (customMonthlyProductSignup.test(path)) { + const [, productId] = path.match(customMonthlyProductSignup); + return { + page: 'signup', + pageQuery: `${productId}/monthly` + }; + } else if (customYearlyProductSignup.test(path)) { + const [, productId] = path.match(customYearlyProductSignup); + return { + page: 'signup', + pageQuery: `${productId}/yearly` + }; + } else if (customPricesSignupRegex.test(path)) { + const [, pageQuery] = path.match(customPricesSignupRegex); + return { + page: 'signup', + pageQuery: pageQuery + }; + } else if (path === 'signup/free') { + return { + page: 'signup', + pageQuery: 'free' + }; + } else if (path === 'signup/monthly') { + return { + page: 'signup', + pageQuery: 'monthly' + }; + } else if (path === 'signup/yearly') { + return { + page: 'signup', + pageQuery: 'yearly' + }; + } else if (path === 'signin') { + return { + page: 'signin' + }; + } else if (path === 'account') { + return { + page: 'accountHome' + }; + } else if (path === 'account/plans') { + return { + page: 'accountPlan' + }; + } else if (path === 'account/profile') { + return { + page: 'accountProfile' + }; + } else if (path === 'account/newsletters') { + return { + page: 'accountEmail' + }; + } else if (path === 'support') { + return { + page: 'support' + }; + } else if (path === 'support/success') { + return { + page: 'supportSuccess' + }; + } else if (path === 'support/error') { + return { + page: 'supportError' + }; + } else if (path === 'recommendations') { + return { + page: 'recommendations', + pageData: { + signup: false + } + }; + } else if (path === 'gift') { + return { + page: 'gift' + }; + } else if (path === 'share') { + return { + page: 'share' + }; + } else if (path === 'account/newsletters/help') { + return { + page: 'emailReceivingFAQ', + pageData: { + direct: true + } + }; + } else if (path === 'account/newsletters/disabled') { + return { + page: 'emailSuppressionFAQ', + pageData: { + direct: true + } + }; + } + + return { + page: 'default' + }; + } + + /**Get Accent color from site data*/ + getAccentColor() { + const {accent_color: accentColor} = this.state.site || {}; + return validateHexColor(accentColor); + } + + getRetentionPreviewMember({site, offers}) { + const retentionOffer = (offers || []).find(offer => isRetentionOffer({offer})); + const productId = retentionOffer?.tier?.id; + const product = productId ? getProductFromId({site, productId}) : null; + const price = product ? (retentionOffer.cadence === 'year' ? product.yearlyPrice : product.monthlyPrice) : null; + const previewMember = Fixtures.member.preview; + const previewSubscription = previewMember?.subscriptions?.[0]; + + if (!previewSubscription || !product || !price) { + return previewMember; + } + + return { + ...previewMember, + subscriptions: [{ + ...previewSubscription, + plan: { + ...previewSubscription.plan, + amount: price.amount, + interval: price.interval, + currency: price.currency.toUpperCase() + }, + price: { + ...previewSubscription.price, + price_id: price.id, + amount: price.amount, + interval: price.interval, + currency: price.currency, + product: { + ...previewSubscription.price?.product, + product_id: product.id + } + }, + tier: {id: product.id, name: product.name} + }] + }; + } + + /**Get final page set in App context from state data*/ + getContextPage({site, page, member}) { + /**Set default page based on logged-in status */ + if (!page || page === 'default') { + const loggedOutPage = isInviteOnly({site}) || !hasAvailablePrices({site}) ? 'signin' : 'signup'; + page = member ? 'accountHome' : loggedOutPage; + } + + return getActivePage({page}); + } + + /**Get final member set in App context from state data*/ + getContextMember({site, page, member, offers, pageData, customSiteUrl}) { + if (hasMode(['dev', 'preview'], {customSiteUrl})) { + /** Use dummy member(free or paid) for account pages in dev/preview mode*/ + if (isAccountPage({page}) || isOfferPage({page})) { + if (hasMode(['dev'], {customSiteUrl})) { + return member || Fixtures.member.free; + } else if (hasMode(['preview'])) { + if (page === 'accountPlan' && pageData?.action === 'cancel') { + return this.getRetentionPreviewMember({site, offers}); + } + + return Fixtures.member.preview; + } else { + return Fixtures.member.paid; + } + } + + /** Ignore member for non-account pages in dev/preview mode*/ + return null; + } + return member; + } + + /**Get final App level context from App state*/ + getContextFromState() { + const {site, member, offers, action, actionErrorMessage, page, lastPage, showPopup, pageQuery, pageData, popupNotification, notification, customSiteUrl, dir, scrollbarWidth, otcRef, inboxLinks} = this.state; + const contextPage = this.getContextPage({site, page, member}); + const contextMember = this.getContextMember({site, page: contextPage, member, offers, pageData, customSiteUrl}); + return { + api: this.GhostApi, + site, + offers, + action, + actionErrorMessage, + brandColor: this.getAccentColor(), + page: contextPage, + pageQuery, + pageData, + member: contextMember, + lastPage, + showPopup, + popupNotification, + notification, + customSiteUrl, + dir, + scrollbarWidth, + otcRef, + inboxLinks, + doAction: (_action, data) => this.dispatchAction(_action, data) + }; + } + + getRecommendationButtons() { + const customTriggerSelector = '[data-recommendation]'; + return document.querySelectorAll(customTriggerSelector) || []; + } + + /** Setup click tracking for recommendation buttons */ + setupRecommendationButtons() { + // Handler for custom buttons + const clickHandler = (event) => { + // Send beacons for recommendation clicks + const recommendationId = event.currentTarget.dataset.recommendation; + + if (recommendationId) { + this.dispatchAction('trackRecommendationClicked', { + recommendationId + // eslint-disable-next-line no-console + }).catch(console.error); + } else { + // eslint-disable-next-line no-console + console.warn('[Portal] Invalid usage of data-recommendation attribute'); + } + }; + + const elements = this.getRecommendationButtons(); + for (const element of elements) { + element.addEventListener('click', clickHandler, {passive: true}); + } + } + + /** + * Transform any portal links to use relative paths + * + * Prevents unwanted/unnecessary switches to the home page when opening the + * portal. Especially useful for copy/pasted links from Admin screens. + */ + transformPortalLinksToRelative() { + document.querySelectorAll('a[href*="#/portal"], a[href*="#/share"]').forEach(transformPortalAnchorToRelative); + } + + render() { + if (this.state.initStatus === 'success') { + return ( + + + + + + + + ); + } + return null; + } +} diff --git a/apps/portal/src/data-attributes.js b/apps/portal/src/data-attributes.js new file mode 100644 index 0000000..60bd2b8 --- /dev/null +++ b/apps/portal/src/data-attributes.js @@ -0,0 +1,490 @@ +/* eslint-disable no-console */ +import {getCheckoutSessionDataFromPlanAttribute, getUrlHistory} from './utils/helpers'; +import {HumanReadableError, chooseBestErrorMessage} from './utils/errors'; +import {t} from './utils/i18n'; + +function displayErrorIfElementExists(errorEl, message) { + if (errorEl) { + errorEl.innerText = message; + } +} + +function handleError(error, form, errorEl) { + form.classList.add('error'); + const defaultMessage = t('There was an error sending the email, please try again'); + displayErrorIfElementExists(errorEl, chooseBestErrorMessage(error, defaultMessage)); +} + +export async function formSubmitHandler( + {event, form, errorEl, siteUrl, submitHandler, doAction, captureException} +) { + form.removeEventListener('submit', submitHandler); + event.preventDefault(); + if (errorEl) { + errorEl.innerText = ''; + } + form.classList.remove('success', 'invalid', 'error'); + let emailInput = event.target.querySelector('input[data-members-email]'); + let nameInput = event.target.querySelector('input[data-members-name]'); + let autoRedirect = form?.dataset?.membersAutoredirect || 'true'; + let email = emailInput?.value; + let name = (nameInput?.value || '').trim() || undefined; + let emailType = undefined; + let labels = []; + let newsletters = []; + + let labelInputs = event.target.querySelectorAll('input[data-members-label]') || []; + for (let i = 0; i < labelInputs.length; ++i) { + labels.push(labelInputs[i].value); + } + + let newsletterInputs = event.target.querySelectorAll('input[type=hidden][data-members-newsletter], input[type=checkbox][data-members-newsletter]:checked, input[type=radio][data-members-newsletter]:checked') || []; + for (let i = 0; i < newsletterInputs.length; ++i) { + newsletters.push({name: newsletterInputs[i].value}); + } + + if (form.dataset.membersForm) { + emailType = form.dataset.membersForm; + } + + const wantsOTC = emailType === 'signin' && form?.dataset?.membersOtc === 'true'; + + form.classList.add('loading'); + const urlHistory = getUrlHistory(); + const reqBody = { + email: email, + emailType: emailType, + labels: labels, + name: name, + autoRedirect: (autoRedirect === 'true') + }; + if (wantsOTC) { + reqBody.includeOTC = true; + } + if (urlHistory) { + reqBody.urlHistory = urlHistory; + } + if (newsletterInputs.length > 0) { + reqBody.newsletters = newsletters; + } else { + // If there was only check-able newsletter inputs in the form, but none were checked, set reqBody.newsletters + // to an empty array so that the member is not signed up to the default newsletters + const checkableNewsletterInputs = event.target.querySelectorAll('input[type=checkbox][data-members-newsletter]') || []; + + if (checkableNewsletterInputs.length > 0) { + reqBody.newsletters = []; + } + } + + try { + const integrityTokenRes = await fetch(`${siteUrl}/members/api/integrity-token/`, {method: 'GET'}); + const integrityToken = await integrityTokenRes.text(); + + const magicLinkRes = await fetch(`${siteUrl}/members/api/send-magic-link/`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({...reqBody, integrityToken}) + }); + + form.addEventListener('submit', submitHandler); + form.classList.remove('loading'); + if (magicLinkRes.ok) { + form.classList.add('success'); + + let responseBody; + if (wantsOTC) { + try { + responseBody = await magicLinkRes.clone().json(); + } catch (e) { + responseBody = undefined; + } + } + + const otcRef = responseBody?.otc_ref; + if (otcRef && typeof doAction === 'function') { + try { + doAction('startSigninOTCFromCustomForm', { + email: (email || '').trim(), + otcRef, + inboxLinks: responseBody?.inboxLinks + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + captureException?.(e); + } + } + } else { + const e = await HumanReadableError.fromApiResponse(magicLinkRes); + const errorMessage = chooseBestErrorMessage(e, t('Failed to send magic link email')); + displayErrorIfElementExists(errorEl, errorMessage); + form.classList.add('error'); // Ensure error state is set here + } + } catch (err) { + handleError(err, form, errorEl); + } +} + +export function planClickHandler({event, el, errorEl, siteUrl, site, member, clickHandler}) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + let plan = el.dataset.membersPlan; + let requestData = getCheckoutSessionDataFromPlanAttribute(site, plan.toLowerCase()); + let successUrl = el.dataset.membersSuccess; + let cancelUrl = el.dataset.membersCancel; + let checkoutSuccessUrl; + let checkoutCancelUrl; + + if (successUrl) { + checkoutSuccessUrl = (new URL(successUrl, window.location.href)).href; + } + + if (cancelUrl) { + checkoutCancelUrl = (new URL(cancelUrl, window.location.href)).href; + } + + if (errorEl) { + errorEl.innerText = ''; + } + el.classList.add('loading'); + const metadata = member ? { + checkoutType: 'upgrade' + } : {}; + const urlHistory = getUrlHistory(); + + if (urlHistory) { + metadata.urlHistory = urlHistory; + } + + return fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/create-stripe-checkout-session/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...requestData, + identity: identity, + successUrl: checkoutSuccessUrl, + cancelUrl: checkoutCancelUrl, + metadata + }) + }).then(function (res) { + if (!res.ok) { + throw new Error(t('Could not create Stripe checkout session')); + } + return res.json(); + }); + }).then(function (responseBody) { + if (responseBody.url) { + return window.location.assign(responseBody.url); + } + const stripe = window.Stripe(responseBody.publicKey); + return stripe.redirectToCheckout({ + sessionId: responseBody.sessionId + }).then(function (redirectResult) { + if (redirectResult.error) { + throw new Error(redirectResult.error.message); + } + }); + }).catch(function (err) { + console.error(err); + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + if (errorEl) { + errorEl.innerText = err.message; + } + el.classList.add('error'); + }); +} + +export function handleDataAttributes({siteUrl, site = {}, member, offers = [], doAction, captureException} = {}) { + if (!siteUrl) { + return; + } + + siteUrl = siteUrl.replace(/\/$/, ''); + Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'), function (form) { + let errorEl = form.querySelector('[data-members-error]'); + function submitHandler(event) { + formSubmitHandler({event, errorEl, form, siteUrl, submitHandler, doAction, captureException}); + } + form.addEventListener('submit', submitHandler); + }); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-plan]'), function (el) { + let errorEl = el.querySelector('[data-members-error]'); + function clickHandler(event) { + planClickHandler({el, event, errorEl, member, site, siteUrl, clickHandler}); + } + el.addEventListener('click', clickHandler); + }); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-edit-billing]'), function (el) { + let errorEl = el.querySelector('[data-members-error]'); + let membersSuccess = el.dataset.membersSuccess; + let membersCancel = el.dataset.membersCancel; + let successUrl; + let cancelUrl; + + if (membersSuccess) { + successUrl = (new URL(membersSuccess, window.location.href)).href; + } + + if (membersCancel) { + cancelUrl = (new URL(membersCancel, window.location.href)).href; + } + + function clickHandler(event) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + + if (errorEl) { + errorEl.innerText = ''; + } + el.classList.add('loading'); + fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/create-stripe-update-session/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: identity, + successUrl: successUrl, + cancelUrl: cancelUrl + }) + }).then(function (res) { + if (!res.ok) { + throw new Error(t('Could not create Stripe checkout session')); + } + return res.json(); + }); + }).then(function (result) { + let stripe = window.Stripe(result.publicKey); + return stripe.redirectToCheckout({ + sessionId: result.sessionId + }); + }).then(function (result) { + if (result.error) { + throw new Error(t(result.error.message)); + } + }).catch(function (err) { + console.error(err); + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + if (errorEl) { + errorEl.innerText = err.message; + } + el.classList.add('error'); + }); + } + el.addEventListener('click', clickHandler); + }); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-manage-billing]'), function (el) { + let errorEl = el.querySelector('[data-members-error]'); + let membersReturn = el.dataset.membersReturn; + let returnUrl; + + if (membersReturn) { + returnUrl = (new URL(membersReturn, window.location.href)).href; + } + + function clickHandler(event) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + + if (errorEl) { + errorEl.innerText = ''; + } + el.classList.add('loading'); + fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/create-stripe-billing-portal-session/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: identity, + returnUrl + }) + }).then(function (res) { + if (!res.ok) { + throw new Error(t('Could not create Stripe billing portal session')); + } + return res.json(); + }); + }).then(function (result) { + return window.location.assign(result.url); + }).catch(function (err) { + console.error(err); + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + if (errorEl) { + errorEl.innerText = err.message; + } + el.classList.add('error'); + }); + } + el.addEventListener('click', clickHandler); + }); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-signout]'), function (el) { + function clickHandler(event) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + el.classList.remove('error'); + el.classList.add('loading'); + fetch(`${siteUrl}/members/api/session`, { + method: 'DELETE' + }).then(function (res) { + if (res.ok) { + window.location.replace(siteUrl); + } else { + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + el.classList.add('error'); + } + }); + } + el.addEventListener('click', clickHandler); + }); + + const hasRetentionOffers = (offers || []).some(offer => offer.redemption_type === 'retention'); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-cancel-subscription]'), function (el) { + let errorEl = el.parentElement.querySelector('[data-members-error]'); + function clickHandler(event) { + event.preventDefault(); + + let subscriptionId = el.dataset.membersCancelSubscription; + + // If retention offer is available, open Portal to show the offer + if (hasRetentionOffers) { + doAction('openPopup', { + page: 'accountPlan', + pageData: { + subscriptionId, + action: 'cancel' + } + }); + + return; + } + + el.removeEventListener('click', clickHandler); + el.classList.remove('error'); + el.classList.add('loading'); + + if (errorEl) { + errorEl.innerText = ''; + } + + return fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/subscriptions/${subscriptionId}/`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: identity, + smart_cancel: true + }) + }); + }).then(function (res) { + if (res.ok) { + window.location.reload(); + } else { + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + el.classList.add('error'); + + if (errorEl) { + errorEl.innerText = t('There was an error cancelling your subscription, please try again.'); + } + } + }); + } + el.addEventListener('click', clickHandler); + }); + + Array.prototype.forEach.call(document.querySelectorAll('[data-members-continue-subscription]'), function (el) { + let errorEl = el.parentElement.querySelector('[data-members-error]'); + function clickHandler(event) { + el.removeEventListener('click', clickHandler); + event.preventDefault(); + el.classList.remove('error'); + el.classList.add('loading'); + + let subscriptionId = el.dataset.membersContinueSubscription; + + if (errorEl) { + errorEl.innerText = ''; + } + + return fetch(`${siteUrl}/members/api/session`, { + credentials: 'same-origin' + }).then(function (res) { + if (!res.ok) { + return null; + } + + return res.text(); + }).then(function (identity) { + return fetch(`${siteUrl}/members/api/subscriptions/${subscriptionId}/`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + identity: identity, + cancel_at_period_end: false + }) + }); + }).then(function (res) { + if (res.ok) { + window.location.reload(); + } else { + el.addEventListener('click', clickHandler); + el.classList.remove('loading'); + el.classList.add('error'); + + if (errorEl) { + errorEl.innerText = t('There was an error continuing your subscription, please try again.'); + } + } + }); + } + el.addEventListener('click', clickHandler); + }); +} diff --git a/apps/portal/src/index.css b/apps/portal/src/index.css new file mode 100644 index 0000000..e69de29 diff --git a/apps/portal/src/index.js b/apps/portal/src/index.js new file mode 100644 index 0000000..fddcb42 --- /dev/null +++ b/apps/portal/src/index.js @@ -0,0 +1,55 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './app'; + +const ROOT_DIV_ID = 'ghost-portal-root'; + +function addRootDiv() { + const elem = document.createElement('div'); + elem.id = ROOT_DIV_ID; + elem.setAttribute('data-testid', 'portal-root'); + document.body.appendChild(elem); +} + +function getSiteData() { + /** + * @type {HTMLElement} + */ + const scriptTag = document.querySelector('script[data-ghost]'); + if (scriptTag) { + const siteI18nEnabled = scriptTag.dataset.i18n === 'true'; + const siteUrl = scriptTag.dataset.ghost; + const apiKey = scriptTag.dataset.key; + const apiUrl = scriptTag.dataset.api; + const locale = scriptTag.dataset.locale; // not providing a fallback here but will do it within the app. + return {siteUrl, apiKey, apiUrl, siteI18nEnabled, locale}; + } + return {}; +} + +function handleTokenUrl() { + const url = new URL(window.location.href); + if (url.searchParams.get('token')) { + url.searchParams.delete('token'); + window.history.replaceState({}, document.title, url.href); + } +} + +function init() { + // const customSiteUrl = getSiteUrl(); + const {siteUrl: customSiteUrl, apiKey, apiUrl, siteI18nEnabled, locale} = getSiteData(); + const siteUrl = customSiteUrl || window.location.origin; + + addRootDiv(); + handleTokenUrl(); + + ReactDOM.render( + + + , + document.getElementById(ROOT_DIV_ID) + ); +} + +init(); diff --git a/apps/portal/src/logo.svg b/apps/portal/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/apps/portal/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/portal/src/pages.js b/apps/portal/src/pages.js new file mode 100644 index 0000000..c80dc44 --- /dev/null +++ b/apps/portal/src/pages.js @@ -0,0 +1,74 @@ +import SigninPage from './components/pages/signin-page'; +import SignupPage from './components/pages/signup-page'; +import AccountHomePage from './components/pages/AccountHomePage/account-home-page'; +import MagicLinkPage from './components/pages/magic-link-page'; +import LoadingPage from './components/pages/loading-page'; +import AccountPlanPage from './components/pages/account-plan-page'; +import AccountProfilePage from './components/pages/account-profile-page'; +import AccountEmailPage from './components/pages/account-email-page'; +import OfferPage from './components/pages/offer-page'; +import NewsletterSelectionPage from './components/pages/newsletter-selection-page'; +import UnsubscribePage from './components/pages/unsubscribe-page'; +import FeedbackPage from './components/pages/feedback-page'; +import EmailSuppressedPage from './components/pages/email-suppressed-page'; +import EmailSuppressionFAQ from './components/pages/email-suppression-faq'; +import EmailReceivingFAQ from './components/pages/email-receiving-faq'; +import SupportPage from './components/pages/support-page'; +import SupportSuccess from './components/pages/support-success'; +import SupportError from './components/pages/support-error'; +import RecommendationsPage from './components/pages/recommendations-page'; +import GiftPage from './components/pages/gift-page'; +import GiftRedemptionPage from './components/pages/gift-redemption-page'; +import GiftSuccessPage from './components/pages/gift-success-page'; +import ShareModal from './components/pages/share/share-modal'; + +/** List of all available pages in Portal, mapped to their UI component + * Any new page added to portal needs to be mapped here +*/ +const Pages = { + signin: SigninPage, + signup: SignupPage, + accountHome: AccountHomePage, + accountPlan: AccountPlanPage, + accountProfile: AccountProfilePage, + accountEmail: AccountEmailPage, + signupNewsletter: NewsletterSelectionPage, + unsubscribe: UnsubscribePage, + magiclink: MagicLinkPage, + loading: LoadingPage, + offer: OfferPage, + feedback: FeedbackPage, + emailSuppressed: EmailSuppressedPage, + emailSuppressionFAQ: EmailSuppressionFAQ, + emailReceivingFAQ: EmailReceivingFAQ, + support: SupportPage, + supportSuccess: SupportSuccess, + supportError: SupportError, + recommendations: RecommendationsPage, + gift: GiftPage, + giftRedemption: GiftRedemptionPage, + giftSuccess: GiftSuccessPage, + share: ShareModal +}; + +/** Return page if valid, fallback to signup */ +export const getActivePage = function ({page}) { + if (Object.keys(Pages).includes(page)) { + return page; + } + return 'signup'; +}; + +export const isAccountPage = function ({page}) { + return page.includes('account'); +}; + +export const isOfferPage = function ({page}) { + return page.includes('offer'); +}; + +export const isSupportPage = function ({page}) { + return page.includes('support'); +}; + +export default Pages; diff --git a/apps/portal/test/actions.test.ts b/apps/portal/test/actions.test.ts new file mode 100644 index 0000000..8ff8d17 --- /dev/null +++ b/apps/portal/test/actions.test.ts @@ -0,0 +1,557 @@ +import ActionHandler from '../src/actions'; +import {vi, type MockInstance} from 'vitest'; + +describe('updateProfile action', () => { + test('trims whitespace from name before saving', async () => { + const mockApi = { + member: { + update: vi.fn(() => Promise.resolve({name: 'John Doe', email: 'john@example.com'})) + } + }; + const state = { + member: {name: 'Old Name', email: 'john@example.com'} + }; + + await ActionHandler({ + action: 'updateProfile', + data: {name: ' John Doe ', email: 'john@example.com'}, + state, + api: mockApi + }); + + expect(mockApi.member.update).toHaveBeenCalledWith({name: 'John Doe'}); + }); +}); + +describe('signup action', () => { + test('trims whitespace from name', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + sendMagicLink: vi.fn(() => Promise.resolve()) + } + }; + const state = {site: {}}; + + await ActionHandler({ + action: 'signup', + data: {plan: 'free', email: 'john@example.com', name: ' John Doe '}, + state, + api: mockApi + }); + + expect(mockApi.member.sendMagicLink).toHaveBeenCalledWith( + expect.objectContaining({name: 'John Doe'}) + ); + }); +}); + +describe('redeemGift action', () => { + test('redeems a gift directly for a logged-in member and refreshes member data', async () => { + const mockApi = { + gift: { + redeem: vi.fn(() => Promise.resolve({ + gifts: [{ + token: 'gift-token-123', + status: 'redeemed' + }] + })) + }, + member: { + sessionData: vi.fn(() => Promise.resolve({ + name: 'Jamie Larson', + email: 'jamie@example.com', + paid: true, + status: 'gift' + })), + getIntegrityToken: vi.fn(), + sendMagicLink: vi.fn() + } + }; + const state = { + member: { + name: 'Jamie Larson', + email: 'jamie@example.com', + status: 'free' + }, + pageData: { + token: 'gift-token-123', + gift: { + cadence: 'year', + duration: 1, + tier: { + name: 'Premium' + } + } + } + }; + + const result = await ActionHandler({ + action: 'redeemGift', + data: { + giftToken: 'gift-token-123' + }, + state, + api: mockApi + }); + + expect(mockApi.gift.redeem).toHaveBeenCalledWith({token: 'gift-token-123'}); + expect(mockApi.member.sessionData).toHaveBeenCalled(); + expect(mockApi.member.getIntegrityToken).not.toHaveBeenCalled(); + expect(mockApi.member.sendMagicLink).not.toHaveBeenCalled(); + expect(result).toMatchObject({ + action: 'redeemGift:success', + page: 'accountHome', + member: { + status: 'gift' + }, + notification: { + type: 'giftRedeem', + status: 'success' + } + }); + }); + + test('sends a subscribe magic link with the gift token and redirects back to Portal account', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + sendMagicLink: vi.fn(() => Promise.resolve({otc_ref: 'otc-ref-123'})) + } + }; + const state = { + site: { + url: 'https://example.com/' + }, + pageData: { + token: 'gift-token-123' + } + }; + + const result = await ActionHandler({ + action: 'redeemGift', + data: { + email: 'jamie@example.com', + name: ' Jamie Larson ', + giftToken: 'gift-token-123' + }, + state, + api: mockApi + }); + + expect(mockApi.member.sendMagicLink).toHaveBeenCalledWith({ + email: 'jamie@example.com', + emailType: 'subscribe', + integrityToken: 'token-123', + includeOTC: true, + redirect: 'https://example.com/#/portal/account?giftRedemption=true', + giftToken: 'gift-token-123', + name: 'Jamie Larson' + }); + + expect(result).toMatchObject({ + page: 'magiclink', + lastPage: 'giftRedemption', + otcRef: 'otc-ref-123', + pageData: { + token: 'gift-token-123', + email: 'jamie@example.com', + redirect: 'https://example.com/#/portal/account?giftRedemption=true' + } + }); + }); +}); + +describe('startSigninOTCFromCustomForm action', () => { + test('opens magic link popup with otcRef', async () => { + const state = { + pageData: {existing: 'data'} + }; + const result = await ActionHandler({ + action: 'startSigninOTCFromCustomForm', + data: { + email: ' test@example.com ', + otcRef: 'ref-123' + }, + state, + api: {} + }); + + expect(result).toMatchObject({ + showPopup: true, + page: 'magiclink', + lastPage: 'signin', + otcRef: 'ref-123', + pageData: { + existing: 'data', + email: 'test@example.com' + }, + popupNotification: null + }); + }); + + test('returns empty object when otcRef missing', async () => { + const result = await ActionHandler({ + action: 'startSigninOTCFromCustomForm', + data: { + email: 'test@example.com' + }, + state: {}, + api: {} + }); + + expect(result).toEqual({}); + }); +}); + +describe('notification actions', () => { + test('increments notification count after a notification is dismissed', async () => { + const firstNotification = await ActionHandler({ + action: 'openNotification', + data: { + action: 'giftRedemption:failed', + status: 'error', + autoHide: false, + message: 'Gift could not be redeemed' + }, + state: { + notification: null, + notificationSequence: -1 + }, + api: {} + }); + + expect(firstNotification.notification.count).toBe(0); + expect(firstNotification.notificationSequence).toBe(0); + + const dismissedNotification = await ActionHandler({ + action: 'closeNotification', + data: {}, + state: { + ...firstNotification + }, + api: {} + }); + + expect(dismissedNotification).toEqual({ + notification: null + }); + + const secondNotification = await ActionHandler({ + action: 'openNotification', + data: { + action: 'giftRedemption:failed', + status: 'error', + autoHide: false, + message: 'Gift could not be redeemed' + }, + state: { + ...firstNotification, + ...dismissedNotification + }, + api: {} + }); + + expect(secondNotification.notification.count).toBe(1); + expect(secondNotification.notificationSequence).toBe(1); + }); +}); + +describe('continueSubscription action', () => { + test('returns reloadOnPopupClose on success', async () => { + const mockApi = { + member: { + updateSubscription: vi.fn(() => Promise.resolve()), + sessionData: vi.fn(() => Promise.resolve({name: 'Test', email: 'test@example.com'})) + } + }; + + const result = await ActionHandler({ + action: 'continueSubscription', + data: {subscriptionId: 'sub_123'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBe(true); + expect(result.action).toBe('continueSubscription:success'); + }); + + test('does not return reloadOnPopupClose on failure', async () => { + const mockApi = { + member: { + updateSubscription: vi.fn(() => Promise.reject(new Error('API error'))) + } + }; + + const result = await ActionHandler({ + action: 'continueSubscription', + data: {subscriptionId: 'sub_123'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBeUndefined(); + expect(result.action).toBe('continueSubscription:failed'); + }); +}); + +describe('cancelSubscription action', () => { + test('returns reloadOnPopupClose on success', async () => { + const mockApi = { + member: { + updateSubscription: vi.fn(() => Promise.resolve()), + sessionData: vi.fn(() => Promise.resolve({name: 'Test', email: 'test@example.com'})) + } + }; + + const result = await ActionHandler({ + action: 'cancelSubscription', + data: {subscriptionId: 'sub_123', cancellationReason: 'Too expensive'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBe(true); + expect(result.action).toBe('cancelSubscription:success'); + }); + + test('does not return reloadOnPopupClose on failure', async () => { + const mockApi = { + member: { + updateSubscription: vi.fn(() => Promise.reject(new Error('API error'))) + } + }; + + const result = await ActionHandler({ + action: 'cancelSubscription', + data: {subscriptionId: 'sub_123'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBeUndefined(); + expect(result.action).toBe('cancelSubscription:failed'); + }); +}); + +describe('applyOffer action', () => { + test('returns reloadOnPopupClose on success', async () => { + const mockApi = { + member: { + applyOffer: vi.fn(() => Promise.resolve()), + sessionData: vi.fn(() => Promise.resolve({name: 'Test', email: 'test@example.com'})) + } + }; + + const result = await ActionHandler({ + action: 'applyOffer', + data: {offerId: 'offer_123', subscriptionId: 'sub_123'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBe(true); + expect(result.action).toBe('applyOffer:success'); + }); + + test('does not return reloadOnPopupClose on failure', async () => { + const mockApi = { + member: { + applyOffer: vi.fn(() => Promise.reject(new Error('API error'))) + } + }; + + const result = await ActionHandler({ + action: 'applyOffer', + data: {offerId: 'offer_123', subscriptionId: 'sub_123'}, + state: {}, + api: mockApi + }); + + expect(result.reloadOnPopupClose).toBeUndefined(); + expect(result.action).toBe('applyOffer:failed'); + }); +}); + +describe('verifyOTC action', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let originalLocation: any; + let mockLocationAssign: MockInstance; + + beforeEach(() => { + mockLocationAssign = vi.fn(); + originalLocation = window.location; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window.location = {assign: mockLocationAssign} as any; + }); + + afterEach(() => { + window.location = originalLocation; + }); + + test('redirects on successful verification', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + verifyOTC: vi.fn(() => Promise.resolve({ + redirectUrl: 'https://example.com/success' + })) + } + }; + + await ActionHandler({ + action: 'verifyOTC', + data: {otc: '123456', otcRef: 'ref-123'}, + state: {}, + api: mockApi + }); + + expect(mockLocationAssign).toHaveBeenCalledWith('https://example.com/success'); + expect(mockApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'ref-123', + integrityToken: 'token-123' + }); + }); + + test('returns actionErrorMessage when verification fails without redirectUrl', async () => { + // Simulate API returning parsed JSON without redirectUrl (error case) + const mockResponse = { + errors: [{ + message: 'Invalid verification code' + }] + }; + + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + verifyOTC: vi.fn(() => Promise.resolve(mockResponse)) + } + }; + + const result = await ActionHandler({ + action: 'verifyOTC', + data: {otc: '000000', otcRef: 'ref-123'}, + state: {}, + api: mockApi + }); + + expect(result.action).toBe('verifyOTC:failed'); + expect(result.actionErrorMessage).toBe('Invalid verification code'); + expect(result.popupNotification).toBeUndefined(); + }); + + test('returns actionErrorMessage on API exception', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + verifyOTC: vi.fn(() => Promise.reject(new Error('Network error'))) + } + }; + + const result = await ActionHandler({ + action: 'verifyOTC', + data: {otc: '123456', otcRef: 'ref-123'}, + state: {}, + api: mockApi + }); + + expect(result.action).toBe('verifyOTC:failed'); + expect(result.actionErrorMessage).toBe('Failed to verify code, please try again'); + expect(result.popupNotification).toBeUndefined(); + }); + + test('passes redirect parameter to verifyOTC API call, includes integrity token', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('integrity-123')), + verifyOTC: vi.fn(() => Promise.resolve({ + redirectUrl: 'https://example.com/custom' + })) + } + }; + + await ActionHandler({ + action: 'verifyOTC', + data: { + otc: '123456', + otcRef: 'ref-123', + redirect: 'https://custom-redirect.com' + }, + state: {}, + api: mockApi + }); + + expect(mockApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'ref-123', + redirect: 'https://custom-redirect.com', + integrityToken: 'integrity-123' + }); + }); + + describe('edge cases', () => { + test('handles response without redirectUrl or message', async () => { + const mockApi = { + member: { + getIntegrityToken: vi.fn(() => Promise.resolve('token-123')), + verifyOTC: vi.fn(() => Promise.resolve({})) // empty response + } + }; + + const result = await ActionHandler({ + action: 'verifyOTC', + data: {otc: '123456', otcRef: 'ref-123'}, + state: {}, + api: mockApi + }); + + expect(result.action).toBe('verifyOTC:failed'); + expect(result.actionErrorMessage).toBeDefined(); + }); + }); +}); + +describe('checkoutGift action', () => { + test('calls api.member.checkoutGift with correct data', async () => { + const mockApi = { + member: { + checkoutGift: vi.fn(() => Promise.resolve()) + } + }; + + const result = await ActionHandler({ + action: 'checkoutGift', + data: {tierId: 'tier_123', cadence: 'month'}, + state: {}, + api: mockApi + }); + + expect(mockApi.member.checkoutGift).toHaveBeenCalledWith({ + tierId: 'tier_123', + cadence: 'month' + }); + expect(result.action).toBe('checkoutGift:success'); + }); + + test('returns failed action with notification on error', async () => { + const mockApi = { + member: { + checkoutGift: vi.fn(() => Promise.reject(new Error('Stripe error'))) + } + }; + + const result = await ActionHandler({ + action: 'checkoutGift', + data: {tierId: 'tier_123', cadence: 'month'}, + state: {}, + api: mockApi + }); + + expect(result.action).toBe('checkoutGift:failed'); + expect(result.popupNotification).toBeDefined(); + expect(result.popupNotification.type).toBe('checkoutGift:failed'); + expect(result.popupNotification.status).toBe('error'); + }); +}); diff --git a/apps/portal/test/api.test.js b/apps/portal/test/api.test.js new file mode 100644 index 0000000..91309f8 --- /dev/null +++ b/apps/portal/test/api.test.js @@ -0,0 +1,116 @@ +import setupGhostApi from '../src/utils/api'; +import {HumanReadableError} from '../src/utils/errors'; +import {vi} from 'vitest'; + +describe('Portal API gift redemption', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + test('returns the gifts api payload for redeemable gift tokens', async () => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + gifts: [{ + token: 'gift-token-123' + }] + }), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + })); + + const response = await ghostApi.gift.fetchRedemptionData({token: 'gift-token-123'}); + + expect(response.gifts[0].token).toBe('gift-token-123'); + expect(window.fetch).toHaveBeenCalledWith('https://example.com/members/api/gifts/gift-token-123/redeem/', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: undefined + }); + }); + + test('throws a human-readable error for 400 members api gift responses', async () => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + errors: [{ + message: 'This gift has expired.' + }] + }), { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + })); + + await expect(ghostApi.gift.fetchRedemptionData({token: 'gift-token-123'})).rejects.toEqual(new HumanReadableError('This gift has expired.')); + }); + + test('preserves the api error message for 404 members api gift responses', async () => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + errors: [{ + message: 'Gift not found.' + }] + }), { + status: 404, + headers: { + 'Content-Type': 'application/json' + } + })); + + await expect(ghostApi.gift.fetchRedemptionData({token: 'gift-token-123'})).rejects.toEqual(new HumanReadableError('Gift not found.')); + }); + + test('redeems a gift for a logged-in member via POST', async () => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + gifts: [{ + token: 'gift-token-123', + status: 'redeemed', + consumes_at: '2030-01-01T00:00:00.000Z' + }] + }), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + })); + + const response = await ghostApi.gift.redeem({token: 'gift-token-123'}); + + expect(response.gifts[0].status).toBe('redeemed'); + expect(window.fetch).toHaveBeenCalledWith('https://example.com/members/api/gifts/gift-token-123/redeem/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: '{}' + }); + }); + + test('throws a human-readable error for 400 members api gift redeem responses', async () => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + vi.spyOn(window, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + errors: [{ + message: 'This gift has already been redeemed.' + }] + }), { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + })); + + await expect(ghostApi.gift.redeem({token: 'gift-token-123'})).rejects.toEqual(new HumanReadableError('This gift has already been redeemed.')); + }); +}); diff --git a/apps/portal/test/app-frames.test.js b/apps/portal/test/app-frames.test.js new file mode 100644 index 0000000..100dccc --- /dev/null +++ b/apps/portal/test/app-frames.test.js @@ -0,0 +1,36 @@ +import {render} from '@testing-library/react'; +import {site} from '../src/utils/fixtures'; +import App from '../src/app'; + +const setup = async () => { + const testState = { + site, + member: null, + action: 'init:success', + brandColor: site.accent_color, + page: 'signup', + initStatus: 'success', + showPopup: true, + commentsIsLoading: false + }; + const {...utils} = render( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = await utils.findByTitle(/portal-popup/i); + return { + popupFrame, + triggerButtonFrame, + ...utils + }; +}; + +describe.skip('App', () => { + test('renders popup and trigger frames', async () => { + const {popupFrame, triggerButtonFrame} = await setup(); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + }); +}); diff --git a/apps/portal/test/app.test.js b/apps/portal/test/app.test.js new file mode 100644 index 0000000..d5303bc --- /dev/null +++ b/apps/portal/test/app.test.js @@ -0,0 +1,368 @@ +import App from '../src/app'; +import setupGhostApi from '../src/utils/api'; +import {appRender, within} from './utils/test-utils'; +import {getPriceData, getProductData, getSiteData} from '../src/utils/fixtures-generator'; +import {site as FixtureSite, member as FixtureMember} from './utils/test-fixtures'; +import i18n from '../src/utils/i18n'; +import {vi} from 'vitest'; + +vi.mock('../src/utils/i18n', () => ({ + default: { + changeLanguage: vi.fn(), + dir: vi.fn(), + t: vi.fn(str => str) + }, + t: vi.fn(str => str) +})); + +const createDeferred = () => { + let resolve; + const promise = new Promise((res) => { + resolve = res; + }); + + return { + promise, + resolve + }; +}; + +describe('App', function () { + beforeEach(function () { + // Stub window.location with a URL object so we have an expected origin + const location = new URL('http://example.com'); + delete window.location; + window.location = location; + }); + + function setupApi({site = {}, member = {}} = {}) { + const defaultSite = FixtureSite.singleTier.basic; + const defaultMember = FixtureMember.free; + + const siteFixtures = { + ...defaultSite, + ...site + }; + + const memberFixtures = { + ...defaultMember, + ...member + }; + + const ghostApi = setupGhostApi({siteUrl: 'http://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site: siteFixtures, + member: memberFixtures + }); + }); + + return ghostApi; + } + + test('transforms portal links on render', async () => { + const link = document.createElement('a'); + link.setAttribute('href', 'http://example.com/#/portal/signup'); + document.body.appendChild(link); + + const ghostApi = setupApi(); + const utils = appRender( + + ); + + await utils.findByTitle(/portal-popup/i); + + expect(link.getAttribute('href')).toBe('#/portal/signup'); + }); + + test('transforms share links on render', async () => { + const link = document.createElement('a'); + link.setAttribute('href', 'http://example.com/#/share'); + document.body.appendChild(link); + + const ghostApi = setupApi(); + const utils = appRender( + + ); + + await utils.findByTitle(/portal-popup/i); + + expect(link.getAttribute('href')).toBe('#/share'); + }); + + test('shows gift redemption success notification when popup is open on load', async () => { + window.location = new URL('http://example.com/?action=subscribe&success=true#/portal/account?giftRedemption=true'); + + const ghostApi = setupApi(); + const utils = appRender( + + ); + + const popupFrame = await utils.findByTitle(/portal-popup/i); + const notificationFrame = await utils.findByTitle(/portal-notification/i); + + expect(popupFrame).toBeInTheDocument(); + expect(notificationFrame).toBeInTheDocument(); + expect(within(notificationFrame.contentDocument).getByText('Gift redeemed! You\'re all set.')).toBeInTheDocument(); + }); + + test('shows gift redemption error notification when popup is open on load', async () => { + window.location = new URL('http://example.com/?action=subscribe&success=false#/portal/account?giftRedemption=true'); + + const ghostApi = setupApi(); + const utils = appRender( + + ); + + const popupFrame = await utils.findByTitle(/portal-popup/i); + const notificationFrame = await utils.findByTitle(/portal-notification/i); + + expect(popupFrame).toBeInTheDocument(); + expect(notificationFrame).toBeInTheDocument(); + expect(within(notificationFrame.contentDocument).getByText('We couldn\'t redeem this gift for your account.')).toBeInTheDocument(); + }); + + test('prefers locale prop over site locale for i18n language', async () => { + const ghostApi = setupApi({ + site: { + locale: 'de' + } + }); + + const utils = appRender( + + ); + + await utils.findByTitle(/portal-popup/i); + + i18n.changeLanguage.mock.calls.forEach((call) => { + expect(call[0]).toBe('en'); + }); + }); + + test('reloads page when popup closes with reloadOnPopupClose flag', () => { + const app = new App({siteUrl: 'http://example.com'}); + + window.location.reload = vi.fn(); + + app.state = {...app.state, showPopup: false, reloadOnPopupClose: true}; + app.componentDidUpdate({}, {showPopup: true}); + + expect(window.location.reload).toHaveBeenCalledTimes(1); + }); + + test('does not reload when popup closes without reloadOnPopupClose flag', () => { + const app = new App({siteUrl: 'http://example.com'}); + + window.location.reload = vi.fn(); + + app.state = {...app.state, showPopup: false}; + app.componentDidUpdate({}, {showPopup: true}); + + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + test('does not reload when reloadOnPopupClose is false', () => { + const app = new App({siteUrl: 'http://example.com'}); + + window.location.reload = vi.fn(); + + // Set reloadOnPopupClose to false explicitly and close the popup + app.state = {...app.state, showPopup: false, reloadOnPopupClose: false}; + app.componentDidUpdate({}, {showPopup: true}); + + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + test('ignores malformed gift redemption tokens in hash links', async () => { + window.location.hash = '#/portal/gift/redeem/%E0%A4%A'; + + const app = new App({siteUrl: 'http://example.com'}); + app.fetchGiftRedemptionData = vi.fn(); + + const result = await app.fetchLinkData(FixtureSite.singleTier.basic, FixtureMember.free); + + expect(result).toEqual({}); + expect(app.fetchGiftRedemptionData).not.toHaveBeenCalled(); + }); + + test('ignores malformed gift redemption tokens in trigger links', async () => { + const app = new App({siteUrl: 'http://example.com'}); + app.dispatchAction = vi.fn(); + app.fetchGiftRedemptionData = vi.fn(); + app.state = { + ...app.state, + initStatus: 'success', + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}} + }; + + await app.clickHandler({ + preventDefault: vi.fn(), + currentTarget: { + dataset: { + portal: 'gift/redeem/%E0%A4%A' + } + } + }); + + expect(app.fetchGiftRedemptionData).not.toHaveBeenCalled(); + expect(app.dispatchAction).not.toHaveBeenCalled(); + }); + + test('drops stale custom-trigger gift redemption responses', async () => { + const app = new App({siteUrl: 'http://example.com'}); + const firstRequest = createDeferred(); + const secondRequest = createDeferred(); + + app.setState = vi.fn((updatedState) => { + app.state = {...app.state, ...updatedState}; + }); + app.state = { + ...app.state, + initStatus: 'success', + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}} + }; + app.fetchGiftRedemptionData = vi.fn(({token}) => { + return token === 'first-token' ? firstRequest.promise : secondRequest.promise; + }); + + const firstClick = app.clickHandler({ + preventDefault: vi.fn(), + currentTarget: { + dataset: { + portal: 'gift/redeem/first-token' + } + } + }); + const secondClick = app.clickHandler({ + preventDefault: vi.fn(), + currentTarget: { + dataset: { + portal: 'gift/redeem/second-token' + } + } + }); + + secondRequest.resolve({ + page: 'giftRedemption', + pageData: { + token: 'second-token' + } + }); + await secondClick; + + firstRequest.resolve({ + page: 'giftRedemption', + pageData: { + token: 'first-token' + } + }); + await firstClick; + + expect(app.setState).toHaveBeenCalledTimes(1); + expect(app.state.pageData.token).toBe('second-token'); + }); + + test('drops stale hashchange gift redemption responses', async () => { + const app = new App({siteUrl: 'http://example.com'}); + const firstRequest = createDeferred(); + const secondRequest = createDeferred(); + + app.setState = vi.fn((updatedState) => { + app.state = {...app.state, ...updatedState}; + }); + app.state = { + ...app.state, + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + member: FixtureMember.free + }; + app.fetchGiftRedemptionData = vi.fn(({token}) => { + return token === 'first-token' ? firstRequest.promise : secondRequest.promise; + }); + + window.location.hash = '#/portal/gift/redeem/first-token'; + const firstUpdate = app.updateStateForPreviewLinks(); + + window.location.hash = '#/portal/gift/redeem/second-token'; + const secondUpdate = app.updateStateForPreviewLinks(); + + secondRequest.resolve({ + showPopup: true, + page: 'giftRedemption', + pageData: { + token: 'second-token' + } + }); + await secondUpdate; + + firstRequest.resolve({ + showPopup: true, + page: 'giftRedemption', + pageData: { + token: 'first-token' + } + }); + await firstUpdate; + + expect(app.state.pageData.token).toBe('second-token'); + expect(app.setState).toHaveBeenCalledTimes(1); + }); + + test('parses retention offer preview query data into account cancellation flow', () => { + const app = new App({siteUrl: 'http://example.com'}); + const previewData = app.fetchOfferQueryStrData('redemption_type=retention&display_title=Before%2520you%2520go&display_description=Please%2520stay&type=percent&amount=100&duration=repeating&duration_in_months=2&cadence=month&tier_id=product_123&enabled=false'); + + expect(previewData.page).toBe('accountPlan'); + expect(previewData.pageData).toMatchObject({ + action: 'cancel' + }); + expect(previewData.offers).toHaveLength(1); + expect(previewData.offers[0]).toMatchObject({ + display_title: 'Before you go', + display_description: 'Please stay', + redemption_type: 'retention', + type: 'percent', + amount: 100, + duration: 'repeating', + duration_in_months: 2, + cadence: 'month' + }); + expect(previewData.offers[0].tier).toMatchObject({id: 'product_123'}); + }); + + test('uses the selected tier price for retention offer preview members', () => { + window.location.hash = '#/portal/preview/offer'; + + const yearlyPrice = getPriceData({interval: 'year', amount: 25000, currency: 'usd'}); + const paidProduct = getProductData({ + name: 'Pro', + monthlyPrice: getPriceData({interval: 'month', amount: 2500, currency: 'usd'}), + yearlyPrice + }); + const site = getSiteData({ + products: [paidProduct], + portalProducts: [paidProduct.id] + }); + const app = new App({siteUrl: 'http://example.com'}); + const previewData = app.fetchOfferQueryStrData(`redemption_type=retention&display_title=Stay&display_description=Please%2520stay&type=percent&amount=25&duration=forever&duration_in_months=0&cadence=year&tier_id=${paidProduct.id}`); + + app.state = { + ...app.state, + site, + page: previewData.page, + offers: previewData.offers, + pageData: previewData.pageData + }; + + const context = app.getContextFromState(); + const subscription = context.member.subscriptions[0]; + + expect(subscription.price.amount).toBe(yearlyPrice.amount); + expect(subscription.price.interval).toBe('year'); + expect(subscription.price.price_id).toBe(yearlyPrice.id); + expect(subscription.tier).toMatchObject({ + id: paidProduct.id, + name: paidProduct.name + }); + }); +}); diff --git a/apps/portal/test/data-attributes.test.js b/apps/portal/test/data-attributes.test.js new file mode 100644 index 0000000..aa6add7 --- /dev/null +++ b/apps/portal/test/data-attributes.test.js @@ -0,0 +1,1043 @@ +import App from '../src/app'; +import {site as FixturesSite, member as FixtureMember} from './utils/test-fixtures'; +import {fireEvent, appRender, within, waitFor} from './utils/test-utils'; +import setupGhostApi from '../src/utils/api'; +import * as helpers from '../src/utils/helpers'; +import {formSubmitHandler, planClickHandler, handleDataAttributes} from '../src/data-attributes'; +import {getOfferData} from '../src/utils/fixtures-generator'; +import {vi} from 'vitest'; + +// Mock data +function getMockData({newsletterQuerySelectorResult = null} = {}) { + const site = FixturesSite.singleTier.basic; + const member = null; + + const errorEl = { + innerText: '' + }; + const siteUrl = 'https://portal.localhost'; + const submitHandler = () => {}; + const clickHandler = () => {}; + + const form = { + removeEventListener: vi.fn(), + classList: {remove: vi.fn(), add: vi.fn()}, + dataset: {membersForm: 'signup'}, + addEventListener: vi.fn() + }; + vi.spyOn(form.classList, 'add'); + + const element = { + removeEventListener: () => {}, + dataset: { + membersPlan: 'monthly', + membersSuccess: 'https://portal.localhost/success', + membersCancel: 'https://portal.localhost/cancel' + }, + classList: { + remove: () => {}, + add: () => {} + }, + addEventListener: () => {} + }; + + const event = { + preventDefault: () => {}, + target: { + querySelector: (elem) => { + if (elem === 'input[data-members-email]') { + return { + value: 'jamie@example.com' + }; + } + if (elem === 'input[data-members-name]') { + return { + value: 'Jamie Larsen' + }; + } + }, + querySelectorAll: (elem) => { + if (elem === 'input[data-members-label]') { + return [{ + value: 'Gold' + }]; + } + if (elem === 'input[type=hidden][data-members-newsletter], input[type=checkbox][data-members-newsletter]:checked, input[type=radio][data-members-newsletter]:checked' && newsletterQuerySelectorResult) { + return newsletterQuerySelectorResult; + } + } + } + }; + + return { + event, form, siteUrl, submitHandler, errorEl, clickHandler, site, member, element + }; +} + +describe('Member Data attributes:', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock global fetch + vi.spyOn(window, 'fetch').mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json: async () => ({success: true}) + }); + } + + if (url.includes('api/integrity-token')) { + return Promise.resolve({ + ok: true, + text: async () => 'testtoken' + }); + } + + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => { + return 'session-identity'; + } + }); + } + + if (url.includes('create-stripe-checkout-session')) { + return Promise.resolve({ + ok: true, + json: async () => { + return { + publicKey: 'key-xyz' + }; + } + }); + } + return Promise.resolve({}); + }); + + // Mock global Stripe + window.Stripe = () => {}; + vi.spyOn(window, 'Stripe').mockImplementation(() => { + return { + redirectToCheckout: () => { + return Promise.resolve({}); + } + }; + }); + + // Mock url history method + vi.spyOn(helpers, 'getUrlHistory').mockImplementation(() => { + return [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }]; + }); + + // Mock window.location + let locationMock = vi.fn(); + delete window.location; + window.location = {assign: locationMock}; + window.location.href = (new URL('https://portal.localhost')).href; + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + describe('data-members-form', () => { + test('allows free signup', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(window.fetch).toHaveBeenCalledTimes(2); + const expectedBody = JSON.stringify({ + email: 'jamie@example.com', + emailType: 'signup', + labels: ['Gold'], + name: 'Jamie Larsen', + autoRedirect: true, + urlHistory: [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }], + integrityToken: 'testtoken' + }); + expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'}); + }); + + test('trims whitespace from name on signup', async () => { + // Simulate a name input with leading/trailing whitespace + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + const originalQuerySelector = event.target.querySelector; + event.target.querySelector = vi.fn((selector) => { + if (selector === 'input[data-members-name]') { + return {value: ' Jamie Larsen '}; + } + return originalQuerySelector(selector); + }); + + // Capture the request body sent to the API + let capturedRequestBody; + window.fetch.mockImplementation((url, options) => { + if (url.includes('send-magic-link')) { + capturedRequestBody = JSON.parse(options.body); + return Promise.resolve({ok: true, json: async () => ({success: true})}); + } + if (url.includes('integrity-token')) { + return Promise.resolve({ok: true, text: async () => 'token'}); + } + return Promise.resolve({}); + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(capturedRequestBody.name).toBe('Jamie Larsen'); + }); + + test('requests OTC magic link and opens Portal when flagged with data-members-otc=true', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + form.dataset.membersForm = 'signin'; + form.dataset.membersOtc = 'true'; + + const originalQuerySelector = event.target.querySelector; + event.target.querySelector = vi.fn((selector) => { + if (selector === 'input[data-members-email]') { + return {value: ' jamie@example.com '}; + } + return originalQuerySelector(selector); + }); + + const doAction = vi.fn(() => Promise.resolve()); + + const json = async () => ({otc_ref: 'otc_test_ref'}); + window.fetch.mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json, + clone: () => ({json}) + }); + } + + if (url.includes('integrity-token')) { + return Promise.resolve({ + ok: true, + text: async () => 'testtoken' + }); + } + + return Promise.resolve({ok: true}); + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler, doAction}); + + const magicLinkCall = window.fetch.mock.calls.find(([fetchUrl]) => fetchUrl.includes('send-magic-link')); + const requestBody = JSON.parse(magicLinkCall[1].body); + expect(requestBody.includeOTC).toBe(true); + expect(doAction).toHaveBeenCalledWith('startSigninOTCFromCustomForm', { + email: 'jamie@example.com', + otcRef: 'otc_test_ref' + }); + expect(form.classList.add).toHaveBeenCalledWith('success'); + }); + + test('captures exceptions when OTC action fails', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + form.dataset.membersForm = 'signin'; + form.dataset.membersOtc = 'true'; + + const originalQuerySelector = event.target.querySelector; + event.target.querySelector = vi.fn((selector) => { + if (selector === 'input[data-members-email]') { + return {value: ' jamie@example.com '}; + } + return originalQuerySelector(selector); + }); + + const actionErrorMessage = new Error('failed to start OTC sign-in'); + const doAction = vi.fn(() => { + throw actionErrorMessage; + }); + const captureException = vi.fn(); + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const json = async () => ({otc_ref: 'otc_test_ref'}); + window.fetch.mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json, + clone: () => ({json}) + }); + } + + if (url.includes('integrity-token')) { + return Promise.resolve({ + ok: true, + text: async () => 'testtoken' + }); + } + + return Promise.resolve({ok: true}); + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler, doAction, captureException}); + + expect(doAction).toHaveBeenCalledWith('startSigninOTCFromCustomForm', { + email: 'jamie@example.com', + otcRef: 'otc_test_ref' + }); + expect(captureException).toHaveBeenCalledWith(actionErrorMessage); + expect(consoleSpy).toHaveBeenCalledWith(actionErrorMessage); + expect(form.classList.add).toHaveBeenCalledWith('success'); + + consoleSpy.mockRestore(); + }); + }); + + describe('data-members-plan', () => { + test('allows new member paid signup via direct checkout', async () => { + const {event, errorEl, siteUrl, clickHandler, site, member, element} = getMockData(); + + const paidTier = site.products.find(p => p.type === 'paid'); + + await planClickHandler({event, errorEl, siteUrl, clickHandler, site, member, el: element}); + expect(window.fetch).toHaveBeenNthCalledWith(1, + 'https://portal.localhost/members/api/session', { + credentials: 'same-origin' + } + ); + const expectedBody = { + cadence: 'month', + tierId: paidTier.id, + identity: 'session-identity', + successUrl: 'https://portal.localhost/success', + cancelUrl: 'https://portal.localhost/cancel', + metadata: { + urlHistory: [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }] + } + }; + expect(window.fetch).toHaveBeenNthCalledWith(2, + 'https://portal.localhost/members/api/create-stripe-checkout-session/', { + body: JSON.stringify(expectedBody), + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + } + ); + }); + + test('allows free member upgrade via direct checkout', async () => { + let {event, errorEl, siteUrl, clickHandler, site, member, element} = getMockData(); + member = FixtureMember.free; + const paidTier = site.products.find(p => p.type === 'paid'); + + await planClickHandler({event, errorEl, siteUrl, clickHandler, site, member, el: element}); + expect(window.fetch).toHaveBeenNthCalledWith(1, 'https://portal.localhost/members/api/session', { + credentials: 'same-origin' + }); + const expectedBody = { + cadence: 'month', + tierId: paidTier.id, + identity: 'session-identity', + successUrl: 'https://portal.localhost/success', + cancelUrl: 'https://portal.localhost/cancel', + metadata: { + checkoutType: 'upgrade', + urlHistory: [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }] + } + }; + expect(window.fetch).toHaveBeenNthCalledWith(2, 'https://portal.localhost/members/api/create-stripe-checkout-session/', { + body: JSON.stringify(expectedBody), + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST' + }); + }); + }); + + describe('data-members-manage-billing', () => { + test('opens Stripe billing portal on click', async () => { + const siteUrl = 'https://portal.localhost'; + const billingPortalUrl = 'https://billing.stripe.com/session/test_session'; + + // Setup fetch mock for billing portal + window.fetch.mockImplementation((url) => { + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => 'session-identity' + }); + } + if (url.includes('create-stripe-billing-portal-session')) { + return Promise.resolve({ + ok: true, + json: async () => ({url: billingPortalUrl}) + }); + } + return Promise.resolve({}); + }); + + // Create element with data attribute + document.body.innerHTML = ` + + `; + + handleDataAttributes({siteUrl}); + + const button = document.querySelector('[data-members-manage-billing]'); + button.click(); + + // Wait for promises to resolve + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + + expect(window.fetch).toHaveBeenCalledWith( + 'https://portal.localhost/members/api/session', + {credentials: 'same-origin'} + ); + expect(window.fetch).toHaveBeenCalledWith( + 'https://portal.localhost/members/api/create-stripe-billing-portal-session/', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + identity: 'session-identity', + returnUrl: undefined + }) + } + ); + expect(window.location.assign).toHaveBeenCalledWith(billingPortalUrl); + }); + + test('passes return URL when data-members-return is specified', async () => { + const siteUrl = 'https://portal.localhost'; + const billingPortalUrl = 'https://billing.stripe.com/session/test_session'; + + window.fetch.mockImplementation((url) => { + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => 'session-identity' + }); + } + if (url.includes('create-stripe-billing-portal-session')) { + return Promise.resolve({ + ok: true, + json: async () => ({url: billingPortalUrl}) + }); + } + return Promise.resolve({}); + }); + + document.body.innerHTML = ` + + `; + + handleDataAttributes({siteUrl}); + + const button = document.querySelector('[data-members-manage-billing]'); + button.click(); + + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + + expect(window.fetch).toHaveBeenCalledWith( + 'https://portal.localhost/members/api/create-stripe-billing-portal-session/', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + identity: 'session-identity', + returnUrl: 'https://portal.localhost/account/' + }) + } + ); + }); + + test('adds loading class while processing', async () => { + const siteUrl = 'https://portal.localhost'; + + // Create a promise we can control + window.fetch.mockImplementation((url) => { + if (url.includes('api/session')) { + return new Promise((resolve) => { + resolve({ + ok: true, + text: async () => 'session-identity' + }); + }); + } + return Promise.resolve({}); + }); + + document.body.innerHTML = ` + + `; + + handleDataAttributes({siteUrl}); + + const button = document.querySelector('[data-members-manage-billing]'); + button.click(); + + expect(button.classList.contains('loading')).toBe(true); + }); + + test('shows error and re-enables click handler on API failure', async () => { + const siteUrl = 'https://portal.localhost'; + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + window.fetch.mockImplementation((url) => { + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => 'session-identity' + }); + } + if (url.includes('create-stripe-billing-portal-session')) { + return Promise.resolve({ + ok: false, + json: async () => ({errors: [{message: 'Failed'}]}) + }); + } + return Promise.resolve({}); + }); + + document.body.innerHTML = ` + + `; + + handleDataAttributes({siteUrl}); + + const button = document.querySelector('[data-members-manage-billing]'); + button.click(); + + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + + expect(button.classList.contains('error')).toBe(true); + expect(button.classList.contains('loading')).toBe(false); + + const errorEl = button.querySelector('[data-members-error]'); + expect(errorEl.innerText).toBe('Could not create Stripe billing portal session'); + + consoleSpy.mockRestore(); + }); + + test('handles session fetch failure', async () => { + const siteUrl = 'https://portal.localhost'; + const billingPortalUrl = 'https://billing.stripe.com/session/test_session'; + + window.fetch.mockImplementation((url) => { + if (url.includes('api/session')) { + return Promise.resolve({ + ok: false + }); + } + if (url.includes('create-stripe-billing-portal-session')) { + return Promise.resolve({ + ok: true, + json: async () => ({url: billingPortalUrl}) + }); + } + return Promise.resolve({}); + }); + + document.body.innerHTML = ` + + `; + + handleDataAttributes({siteUrl}); + + const button = document.querySelector('[data-members-manage-billing]'); + button.click(); + + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + + // Should still call billing portal endpoint with null identity + expect(window.fetch).toHaveBeenCalledWith( + 'https://portal.localhost/members/api/create-stripe-billing-portal-session/', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + identity: null, + returnUrl: undefined + }) + } + ); + }); + }); + + describe('data-members-newsletter', () => { + test('includes specified newsletters in request', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData({ + newsletterQuerySelectorResult: [{ + value: 'Some Newsletter' + }] + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(window.fetch).toHaveBeenCalledTimes(2); + const expectedBody = JSON.stringify({ + email: 'jamie@example.com', + emailType: 'signup', + labels: ['Gold'], + name: 'Jamie Larsen', + autoRedirect: true, + urlHistory: [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }], + newsletters: [{name: 'Some Newsletter'}], + integrityToken: 'testtoken' + }); + expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'}); + }); + + test('does not include newsletters in request if there are no newsletter inputs', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData({ + newsletterQuerySelectorResult: [] + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(window.fetch).toHaveBeenCalledTimes(2); + const expectedBody = JSON.stringify({ + email: 'jamie@example.com', + emailType: 'signup', + labels: ['Gold'], + name: 'Jamie Larsen', + autoRedirect: true, + urlHistory: [{ + path: '/blog/', + refMedium: null, + refSource: 'ghost-explore', + refUrl: 'https://example.com/blog/', + time: 1611234567890 + }], + integrityToken: 'testtoken' + }); + expect(window.fetch).toHaveBeenLastCalledWith('https://portal.localhost/members/api/send-magic-link/', {body: expectedBody, headers: {'Content-Type': 'application/json'}, method: 'POST'}); + }); + }); + + describe('data-members-cancel-subscription', () => { + test('opens Portal when retention offers exist', () => { + const siteUrl = 'https://portal.localhost'; + const doAction = vi.fn(); + const retentionOffer = getOfferData({redemptionType: 'retention'}); + + document.body.innerHTML = ` + Cancel + `; + + handleDataAttributes({siteUrl, offers: [retentionOffer], doAction}); + + const button = document.querySelector('[data-members-cancel-subscription]'); + button.click(); + + expect(doAction).toHaveBeenCalledWith('openPopup', { + page: 'accountPlan', + pageData: { + subscriptionId: 'sub_123', + action: 'cancel' + } + }); + expect(window.fetch).not.toHaveBeenCalled(); + }); + + test('falls back to direct API call when no retention offers exist', async () => { + const siteUrl = 'https://portal.localhost'; + const doAction = vi.fn(); + + document.body.innerHTML = ` + Cancel + `; + + handleDataAttributes({siteUrl, offers: [], doAction}); + + const button = document.querySelector('[data-members-cancel-subscription]'); + button.click(); + + await waitFor(() => { + expect(window.fetch).toHaveBeenCalled(); + }); + + expect(doAction).not.toHaveBeenCalled(); + expect(window.fetch).toHaveBeenCalledWith( + 'https://portal.localhost/members/api/session', + {credentials: 'same-origin'} + ); + }); + + test('ignores non-retention offers and uses direct API call', async () => { + const siteUrl = 'https://portal.localhost'; + const doAction = vi.fn(); + const signupOffer = getOfferData({redemptionType: 'signup'}); + + document.body.innerHTML = ` + Cancel + `; + + handleDataAttributes({siteUrl, offers: [signupOffer], doAction}); + + const button = document.querySelector('[data-members-cancel-subscription]'); + button.click(); + + await waitFor(() => { + expect(window.fetch).toHaveBeenCalled(); + }); + + expect(doAction).not.toHaveBeenCalled(); + expect(window.fetch).toHaveBeenCalled(); + }); + }); +}); + +const setup = async ({site, member = null, showPopup = true}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + return { + ghostApi, + popupFrame, + triggerButtonFrame, + ...utils + }; +}; + +describe('Portal Data attributes:', () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Mock global fetch + vi.spyOn(window, 'fetch').mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json: async () => ({success: true}) + }); + } + + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => { + return 'session-identity'; + } + }); + } + + if (url.includes('create-stripe-checkout-session')) { + return Promise.resolve({ + ok: true, + json: async () => { + return { + publicKey: 'key-xyz' + }; + } + }); + } + return Promise.resolve({}); + }); + + // Mock global Stripe + window.Stripe = () => {}; + vi.spyOn(window, 'Stripe').mockImplementation(() => { + return { + redirectToCheckout: () => { + return Promise.resolve({}); + } + }; + }); + + // Mock window.location + let locationMock = vi.fn(); + delete window.location; + window.location = {assign: locationMock}; + window.location.href = (new URL('https://portal.localhost')).href; + window.location.hash = ''; + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + describe('data-portal', () => { + test('opens default portal page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + }); + }); + + describe('data-portal=signin', () => { + test('opens Portal signin page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const loginTitle = within(popupFrame.contentDocument).queryByText(/sign in/i); + expect(loginTitle).toBeInTheDocument(); + }); + }); + + describe('data-portal=signup', () => { + test('opens Portal signup page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const loginTitle = within(popupFrame.contentDocument).queryByText(/already a member/i); + expect(loginTitle).toBeInTheDocument(); + }); + }); + + describe('data-portal=signup/:tierid/monthly', () => { + test('opens Portal signup page', async () => { + const siteData = FixturesSite.singleTier.basic; + const paidTier = siteData.products.find(p => p.type === 'paid'); + + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + }); + }); + + describe('data-portal=share', () => { + test('opens Portal share page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const shareTitle = within(popupFrame.contentDocument).queryByText(/^Share$/i); + expect(shareTitle).toBeInTheDocument(); + }); + }); + + describe('data-portal=account', () => { + test('opens Portal account home page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountHomeTitle = within(popupFrame.contentDocument).queryByText(/your account/i); + expect(accountHomeTitle).toBeInTheDocument(); + }); + }); + + describe('data-portal=account/plans', () => { + test('opens Portal account plan page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountPlanTitle = within(popupFrame.contentDocument).queryByText(/choose a plan/i); + expect(accountPlanTitle).toBeInTheDocument(); + }); + }); + + describe('data-portal=account/profile', () => { + test('opens Portal account profile page', async () => { + document.body.innerHTML = ` +
+ `; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixturesSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(popupFrame).not.toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + const portalElement = document.querySelector('[data-portal]'); + fireEvent.click(portalElement); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountProfileTitle = within(popupFrame.contentDocument).queryByText(/account settings/i); + expect(accountProfileTitle).toBeInTheDocument(); + }); + }); + + describe('data-members-error', () => { + test('displays error message when errorEl exists and network error occurs', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + + // Mock fetch to reject with a network error + window.fetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')) + ); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(errorEl.innerText).toBe('There was an error sending the email, please try again'); + expect(form.classList.add).toHaveBeenCalledWith('error'); + expect(window.fetch).toHaveBeenCalledTimes(1); + }); + + test('handles error gracefully when errorEl is null', async () => { + const {event, form, siteUrl, submitHandler} = getMockData(); + + window.fetch.mockImplementationOnce(() => Promise.reject(new Error('Network error')) + ); + + await expect( + formSubmitHandler({event, form, errorEl: null, siteUrl, submitHandler}) + ).resolves.not.toThrow(); + expect(form.classList.add).toHaveBeenCalledWith('error'); + expect(window.fetch).toHaveBeenCalledTimes(1); + }); + + test('handles error when email does not exist', async () => { + const {event, form, errorEl, siteUrl, submitHandler} = getMockData(); + + window.fetch + .mockResolvedValueOnce({ + ok: true, + text: async () => 'testtoken' + }) + .mockResolvedValueOnce({ + ok: false, + json: async () => ({errors: [{message: 'Failed to send magic link email'}]}), + status: 400 + }); + + await formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}); + + expect(window.fetch).toHaveBeenCalledTimes(2); + expect(form.classList.add).toHaveBeenCalledWith('error'); + expect(errorEl.innerText).toBe('Failed to send magic link email'); + }); + }); +}); diff --git a/apps/portal/test/email-subscriptions-flow.test.js b/apps/portal/test/email-subscriptions-flow.test.js new file mode 100644 index 0000000..6f73022 --- /dev/null +++ b/apps/portal/test/email-subscriptions-flow.test.js @@ -0,0 +1,284 @@ +import App from '../src/app.js'; +import {appRender, fireEvent, within, waitFor} from './utils/test-utils'; +import {newsletters as Newsletters, site as FixtureSite, member as FixtureMember} from './utils/test-fixtures'; +import setupGhostApi from '../src/utils/api.js'; +import userEvent from '@testing-library/user-event'; + +const setup = async ({site, member = null, newsletters}, loggedOut = false) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member: loggedOut ? null : member, + newsletters + }); + }); + + ghostApi.member.update = vi.fn(({newsletters: newNewsletters}) => { + return Promise.resolve({ + newsletters: newNewsletters, + enable_comment_notifications: false + }); + }); + + ghostApi.member.newsletters = vi.fn(() => { + return Promise.resolve({ + newsletters + }); + }); + + ghostApi.member.updateNewsletters = vi.fn(({uuid: memberUuid, newsletters: newNewsletters, enableCommentNotifications}) => { + return Promise.resolve({ + uuid: memberUuid, + newsletters: newNewsletters, + enable_comment_notifications: enableCommentNotifications + }); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const triggerButton = within(triggerButtonFrame.contentDocument).getByTestId('portal-trigger-button'); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + const accountHomeTitle = within(popupIframeDocument).queryByText('Your account'); + const viewPlansButton = within(popupIframeDocument).queryByRole('button', {name: 'View plans'}); + const manageSubscriptionsButton = within(popupIframeDocument).queryByRole('button', {name: 'Manage'}); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + triggerButton, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + accountHomeTitle, + viewPlansButton, + manageSubscriptionsButton, + ...utils + }; +}; + +describe('Newsletter Subscriptions', () => { + test('list newsletters to subscribe to', async () => { + const {popupFrame, triggerButtonFrame, accountHomeTitle, manageSubscriptionsButton, popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }); + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(manageSubscriptionsButton).toBeInTheDocument(); + + // unsure why fireEvent has no effect here + await userEvent.click(manageSubscriptionsButton); + + await waitFor(() => { + const newsletter1 = within(popupIframeDocument).queryByText('Newsletter 1'); + const newsletter2 = within(popupIframeDocument).queryByText('Newsletter 2'); + const emailPreferences = within(popupIframeDocument).queryByText('Email preferences'); + + // within(popupIframeDocument).getByText('dslkfjsdlk'); + expect(newsletter1).toBeInTheDocument(); + expect(newsletter2).toBeInTheDocument(); + expect(emailPreferences).toBeInTheDocument(); + }); + }); + + test('toggle subscribing to a newsletter', async () => { + const {ghostApi, popupFrame, triggerButtonFrame, accountHomeTitle, manageSubscriptionsButton, popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }); + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(manageSubscriptionsButton).toBeInTheDocument(); + + await userEvent.click(manageSubscriptionsButton); + + const newsletter1 = within(popupIframeDocument).queryByText('Newsletter 1'); + expect(newsletter1).toBeInTheDocument(); + + // unsubscribe from Newsletter 1 + const subscriptionToggles = within(popupIframeDocument).getAllByTestId('switch-input'); + const newsletter1Toggle = subscriptionToggles[0]; + expect(newsletter1Toggle).toBeInTheDocument(); + await userEvent.click(newsletter1Toggle); + + // verify that subscription to Newsletter 1 was removed + const expectedSubscriptions = Newsletters.filter(n => n.id !== Newsletters[0].id).map(n => ({id: n.id})); + expect(ghostApi.member.update).toHaveBeenLastCalledWith( + {newsletters: expectedSubscriptions} + ); + + const checkboxes = within(popupIframeDocument).getAllByRole('checkbox'); + const newsletter1Checkbox = checkboxes[0]; + const newsletter2Checkbox = checkboxes[1]; + + expect(newsletter1Checkbox).not.toBeChecked(); + expect(newsletter2Checkbox).toBeChecked(); + + // resubscribe to Newsletter 1 + await userEvent.click(newsletter1Toggle); + expect(newsletter1Checkbox).toBeChecked(); + expect(ghostApi.member.update).toHaveBeenLastCalledWith( + {newsletters: Newsletters.reverse().map(n => ({id: n.id}))} + ); + }); + + test('unsubscribe from all newsletters when logged in', async () => { + const {ghostApi, popupFrame, triggerButtonFrame, accountHomeTitle, manageSubscriptionsButton, popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }); + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(manageSubscriptionsButton).toBeInTheDocument(); + await userEvent.click(manageSubscriptionsButton); + const unsubscribeAllButton = within(popupIframeDocument).queryByRole('button', {name: 'Unsubscribe from all emails'}); + expect(unsubscribeAllButton).toBeInTheDocument(); + + fireEvent.click(unsubscribeAllButton); + + expect(ghostApi.member.update).toHaveBeenCalledWith({newsletters: [], enableCommentNotifications: false}); + // Verify the local state shows the newsletter as unsubscribed + const checkboxes = within(popupIframeDocument).getAllByRole('checkbox'); + const newsletter1Checkbox = checkboxes[0]; + const newsletter2Checkbox = checkboxes[1]; + + expect(newsletter1Checkbox).not.toBeChecked(); + expect(newsletter2Checkbox).not.toBeChecked(); + }); + + describe('from the unsubscribe link > UnsubscribePage', () => { + test('unsubscribe via email link while not logged in', async () => { + // Mock window.location + Object.defineProperty(window, 'location', { + value: new URL(`https://portal.localhost/?action=unsubscribe&uuid=${FixtureMember.subbedToNewsletter.uuid}&newsletter=${Newsletters[0].uuid}&key=hashedMemberUuid`), + writable: true + }); + + const {ghostApi, popupFrame, popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }, true); + + // Verify the API was hit to collect subscribed newsletters + expect(ghostApi.member.newsletters).toHaveBeenLastCalledWith( + { + uuid: FixtureMember.subbedToNewsletter.uuid, + key: 'hashedMemberUuid' + } + ); + expect(popupFrame).toBeInTheDocument(); + + expect(within(popupIframeDocument).getByText(/will no longer receive/)).toBeInTheDocument(); + // Verify the local state shows the newsletter as unsubscribed + const checkboxes = within(popupIframeDocument).getAllByRole('checkbox'); + const newsletter1Checkbox = checkboxes[0]; + const newsletter2Checkbox = checkboxes[1]; + + expect(newsletter1Checkbox).not.toBeChecked(); + expect(newsletter2Checkbox).toBeChecked(); + }); + + test('unsubscribe via email link while logged in', async () => { + // Mock window.location + Object.defineProperty(window, 'location', { + value: new URL(`https://portal.localhost/?action=unsubscribe&uuid=${FixtureMember.subbedToNewsletter.uuid}&newsletter=${Newsletters[0].uuid}&key=hashedMemberUuid`), + writable: true + }); + + const {ghostApi, popupFrame, popupIframeDocument, triggerButton, queryByTitle} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }); + + // Verify the API was hit to collect subscribed newsletters + expect(ghostApi.member.newsletters).toHaveBeenLastCalledWith( + { + uuid: FixtureMember.subbedToNewsletter.uuid, + key: 'hashedMemberUuid' + } + ); + // Verify the local state shows the newsletter as unsubscribed + let checkboxes = within(popupIframeDocument).getAllByRole('checkbox'); + let newsletter1Checkbox = checkboxes[0]; + let newsletter2Checkbox = checkboxes[1]; + + expect(within(popupIframeDocument).getByText(/will no longer receive/)).toBeInTheDocument(); + + expect(newsletter1Checkbox).not.toBeChecked(); + expect(newsletter2Checkbox).toBeChecked(); + + // Close the UnsubscribePage popup frame + const popupCloseButton = within(popupIframeDocument).queryByTestId('close-popup'); + await userEvent.click(popupCloseButton); + expect(popupFrame).not.toBeInTheDocument(); + + // Reopen Portal and go to the unsubscribe page + await userEvent.click(triggerButton); + // We have a new popup frame - can't use the old locator from setup + const newPopupFrame = queryByTitle(/portal-popup/i); + expect(newPopupFrame).toBeInTheDocument(); + const newPopupIframeDocument = newPopupFrame.contentDocument; + + // Open the NewsletterManagement page + const manageSubscriptionsButton = within(newPopupIframeDocument).queryByRole('button', {name: 'Manage'}); + await userEvent.click(manageSubscriptionsButton); + + // Verify that the unsubscribed newsletter is shown as unsubscribed in the new popup + checkboxes = within(newPopupIframeDocument).getAllByRole('checkbox'); + newsletter1Checkbox = checkboxes[0]; + newsletter2Checkbox = checkboxes[1]; + expect(newsletter1Checkbox).not.toBeChecked(); + expect(newsletter2Checkbox).toBeChecked(); + }); + + test('unsubscribe link without a key param', async () => { + // Mock window.location + Object.defineProperty(window, 'location', { + value: new URL(`https://portal.localhost/?action=unsubscribe&uuid=${FixtureMember.subbedToNewsletter.uuid}&newsletter=${Newsletters[0].uuid}`), + writable: true + }); + + const {ghostApi, popupFrame, popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlanWithoutStripe, + member: FixtureMember.subbedToNewsletter, + newsletters: Newsletters + }, true); + + // Verify the popup frame is not shown + expect(popupFrame).toBeInTheDocument(); + // Verify the API was hit to collect subscribed newsletters + expect(ghostApi.member.newsletters).not.toHaveBeenCalled(); + // expect sign in page + expect(within(popupIframeDocument).queryByText('Sign in')).toBeInTheDocument(); + }); + }); +}); diff --git a/apps/portal/test/errors.test.js b/apps/portal/test/errors.test.js new file mode 100644 index 0000000..ffa8eec --- /dev/null +++ b/apps/portal/test/errors.test.js @@ -0,0 +1,71 @@ +import {HumanReadableError, chooseBestErrorMessage} from '../src/utils/errors'; +import {vi} from 'vitest'; + +vi.mock('@tryghost/i18n', () => { + const mockT = vi.fn((message, params) => { + if (params?.number) { + return `translated ${message.replace('{number}', params.number)}`; + } + return `translated ${message}`; + }); + + return { + default: vi.fn(() => ({ + t: mockT + })) + }; +}); + +describe('error messages are set correctly', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('handles 400 error without defaultMessage', async () => { + const error = new Response('{"errors":[{"message":"This is a 400 error"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null)).toEqual('translated This is a 400 error'); + }); + + test('handles an error with defaultMessage not a special message', async () => { + const error = new Response('{"errors":[{"message":"This is a 400 error"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + // note that the default message is passed in already-translated. + expect(chooseBestErrorMessage(humanReadableError, 'translated default message')).toEqual('translated default message'); + }); + + test('handles an error with defaultMessage that is a special message', async () => { + const error = new Response('{"errors":[{"message":"Too many attempts try again in {number} minutes."}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, 'this is the default message')).toEqual('translated Too many attempts try again in {number} minutes.'); + }); + + test('handles an error when the message has a number', async () => { + const error = new Response('{"errors":[{"message":"Too many attempts try again in 10 minutes."}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, 'this is the default message')).toEqual('translated Too many attempts try again in 10 minutes.'); + }); + + test('handles a 500 error', async () => { + const error = new Response('{"errors":[{"message":"This is a 500 error"}]}', {status: 500}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null)).toEqual('translated A server error occurred'); + }); + + test('handles a 404 json api error', async () => { + const error = new Response('{"errors":[{"message":"Gift not found."}]}', { + status: 404, + headers: { + 'Content-Type': 'application/json' + } + }); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null)).toEqual('translated Gift not found.'); + }); + + test('gets the magic link error message correctly', async () => { + const error = new Response('{"errors":[{"message":"Failed to send magic link email"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null)).toEqual('translated Failed to send magic link email'); + }); +}); diff --git a/apps/portal/test/feedback-flow.test.js b/apps/portal/test/feedback-flow.test.js new file mode 100644 index 0000000..316aeb2 --- /dev/null +++ b/apps/portal/test/feedback-flow.test.js @@ -0,0 +1,181 @@ +import App from '../src/app.js'; +import {appRender, fireEvent, waitFor, within} from './utils/test-utils'; +import setupGhostApi from '../src/utils/api.js'; +import {getMemberData, getPostsData, getSiteData} from '../src/utils/fixtures-generator.js'; + +const siteData = getSiteData(); +const memberData = getMemberData(); +const posts = getPostsData(); +const postSlug = posts[0].slug; +const postId = posts[0].id; + +const setup = async (site = siteData, member = memberData, loggedOut = false, api = {}) => { + const ghostApi = setupGhostApi({siteUrl: site.url}); + ghostApi.init = api?.init || vi.fn(() => { + return Promise.resolve({ + site, + member: loggedOut ? null : member + }); + }); + ghostApi.feedback.add = api?.add || vi.fn(() => { + return Promise.resolve({ + feedback: [ + { + id: 1, + postId: 1, + memberId: member ? member.uuid : null, + score: 1 + } + ] + }); + }); + + const utils = appRender( + + ); + + // Note: this await is CRITICAL otherwise the iframe won't be loaded + const popupFrame = await utils.findByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + + return { + ghostApi, + popupIframeDocument, + popupFrame, + ...utils + }; +}; + +describe('Feedback Submission Flow', () => { + describe('Valid feedback URL', () => { + describe('Logged in', () => { + test('Autosubmits feedback', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/?uuid=${memberData.uuid}&key=key`), + writable: true + }); + + const {ghostApi, popupFrame, popupIframeDocument} = await setup(); + + expect(popupFrame).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(1); + + within(popupIframeDocument).getByText('Thanks for the feedback!'); + within(popupIframeDocument).getByText('Your input helps shape what gets published.'); + }); + + test('Autosubmits feedback w/o uuid or key params', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/`), + writable: true + }); + const {ghostApi, popupFrame, popupIframeDocument} = await setup(); + + expect(popupFrame).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(1); + within(popupIframeDocument).getByText('Thanks for the feedback!'); + within(popupIframeDocument).getByText('Your input helps shape what gets published.'); + }); + }); + + describe('Logged out', () => { + test('Requires confirmation', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/?uuid=${memberData.uuid}&key=key`), + writable: true + }); + const {ghostApi, popupFrame, popupIframeDocument} = await setup(siteData, null, true); + + expect(popupFrame).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText('Give feedback on this post')).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText('More like this')).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText('Less like this')).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(0); + + const submitBtn = within(popupIframeDocument).getByText('Submit feedback'); + fireEvent.click(submitBtn); + + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(1); + + // the re-render loop is slow to get to the final state + await waitFor(() => { + within(popupIframeDocument).getByText('Thanks for the feedback!'); + within(popupIframeDocument).getByText('Your input helps shape what gets published.'); + }); + }); + + test('Requires login without key', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/?uuid=${memberData.uuid}`), + writable: true + }); + const {ghostApi, popupFrame, popupIframeDocument} = await setup(siteData, null, true); + + expect(popupFrame).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(0); + expect(within(popupIframeDocument).getByText(/Sign in/)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/Sign up/)).toBeInTheDocument(); + }); + + test('Requires login without uuid or key', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/`), + writable: true + }); + const {ghostApi, popupFrame, popupIframeDocument} = await setup(siteData, null, true); + + expect(popupFrame).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(0); + expect(within(popupIframeDocument).getByText(/Sign in/)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/Sign up/)).toBeInTheDocument(); + }); + }); + + test('Error on fail to submit', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/${postSlug}/#/feedback/${postId}/1/?uuid=${memberData.uuid}&key=key`), + writable: true + }); + const mockApi = { + add: vi.fn(() => { + return Promise.reject(new Error('Failed to submit feedback')); + }) + }; + const {ghostApi, popupFrame, popupIframeDocument} = await setup(siteData, memberData, false, mockApi); + + expect(popupFrame).toBeInTheDocument(); + expect(ghostApi.feedback.add).toHaveBeenCalledTimes(1); + expect(within(popupIframeDocument).getByText(/Sorry/)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/There was a problem submitting your feedback/)).toBeInTheDocument(); + }); + }); + + describe('Invalid feedback URL', () => { + test('Redirects logged in members to account settings', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/postslughere/#/feedback/1/1/1/`), + writable: true + }); + const {popupFrame, popupIframeDocument} = await setup(); + + expect(popupFrame).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/Your account/)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/Sign out/)).toBeInTheDocument(); + }); + + test('Redirects logged out users to sign up', async () => { + Object.defineProperty(window, 'location', { + value: new URL(`${siteData.url}/postslughere/#/feedback/1/1/1/`), + writable: true + }); + const {popupFrame, popupIframeDocument} = await setup(siteData, null, true); + + expect(popupFrame).toBeInTheDocument(); + // takes to sign up + await waitFor(() => { + expect(within(popupIframeDocument).getByText(/Name/)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByText(/Email/)).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/apps/portal/test/get-own.test.ts b/apps/portal/test/get-own.test.ts new file mode 100644 index 0000000..ef9db96 --- /dev/null +++ b/apps/portal/test/get-own.test.ts @@ -0,0 +1,27 @@ +import {getOwn} from '../src/utils/get-own'; + +describe('getOwn', () => { + class Person { + name: string; + constructor(name: string) { + this.name = name; + } + getName() { + return this.name; + } + } + + const obj: Record = {foo: 'bar'}; + const person = new Person('McCoo'); + + test('getting "own" properties', () => { + expect(getOwn(obj, 'foo')).toBe('bar'); + expect(getOwn(person, 'name')).toBe('McCoo'); + }); + + test('ignoring inherited properties', () => { + expect(getOwn(obj, 'baz')).toBeUndefined(); + expect(getOwn(obj, 'hasOwnProperty')).toBeUndefined(); + expect(getOwn(person, 'getName')).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/apps/portal/test/portal-links.test.js b/apps/portal/test/portal-links.test.js new file mode 100644 index 0000000..4e8ea0e --- /dev/null +++ b/apps/portal/test/portal-links.test.js @@ -0,0 +1,749 @@ +import App from '../src/app'; +import {site as FixtureSite, member as FixtureMember} from './utils/test-fixtures'; +import {appRender, fireEvent, waitFor, within} from './utils/test-utils'; +import setupGhostApi from '../src/utils/api'; + +const defaultGiftResponse = { + gifts: [ + { + token: 'gift-token-123', + cadence: 'year', + duration: 1, + tier: { + id: 'tier-gift', + name: 'Bronze', + benefits: [ + 'Five great stories to read every day', + 'Videos and podcasts to charm and delight you' + ] + } + } + ] +}; + +const setup = async ({site, member = null, showPopup = true, giftResponse = defaultGiftResponse, giftError = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve('testtoken'); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + ghostApi.member.sessionData = vi.fn(() => { + return Promise.resolve(member); + }); + + ghostApi.gift.fetchRedemptionData = vi.fn(() => { + if (giftError) { + return Promise.reject(giftError); + } + + return Promise.resolve(giftResponse); + }); + + ghostApi.gift.redeem = vi.fn(() => { + return Promise.resolve({ + gifts: [{ + token: giftResponse.gifts[0].token, + status: 'redeemed' + }] + }); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + return { + ghostApi, + popupFrame, + triggerButtonFrame, + ...utils + }; +}; + +describe('Portal Data links:', () => { + beforeEach(() => { + // Mock global fetch + vi.spyOn(window, 'fetch').mockImplementation((url) => { + if (url.includes('send-magic-link')) { + return Promise.resolve({ + ok: true, + json: async () => ({success: true}) + }); + } + + if (url.includes('api/session')) { + return Promise.resolve({ + ok: true, + text: async () => { + return 'session-identity'; + } + }); + } + + if (url.includes('create-stripe-checkout-session')) { + return Promise.resolve({ + ok: true, + json: async () => { + return { + publicKey: 'key-xyz' + }; + } + }); + } + return Promise.resolve({}); + }); + + // Mock global Stripe + window.Stripe = () => {}; + vi.spyOn(window, 'Stripe').mockImplementation(() => { + return { + redirectToCheckout: () => { + return Promise.resolve({}); + } + }; + }); + + // Mock window.location + let locationMock = vi.fn(); + delete window.location; + window.location = {assign: locationMock}; + window.location.href = (new URL('https://portal.localhost')).href; + }); + afterEach(() => { + vi.restoreAllMocks(); + window.location.hash = ''; + }); + describe('#/portal', () => { + test('opens default portal page', async () => { + window.location.hash = '#/portal'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const signupTitle = within(popupFrame.contentDocument).queryByText(/already a member/i); + expect(signupTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/signin', () => { + test('opens portal signin page', async () => { + window.location.hash = '#/portal/signin'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const loginTitle = within(popupFrame.contentDocument).queryByText(/sign in/i); + expect(loginTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/signup', () => { + test('opens portal signup page', async () => { + window.location.hash = '#/portal/signup'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const signupTitle = within(popupFrame.contentDocument).queryByText(/already a member/i); + expect(signupTitle).toBeInTheDocument(); + }); + + describe('on a paid-members only site', () => { + describe('with only a free plan', () => { + test('renders invite-only message and does not allow signups', async () => { + window.location.hash = '#/portal/signup'; + let { + popupFrame + } = await setup({ + site: {...FixtureSite.singleTier.onlyFreePlan, members_signup_access: 'paid'}, + member: null + }); + + expect(popupFrame).toBeInTheDocument(); + + const inviteOnlyMessage = within(popupFrame.contentDocument).queryByText(/This site is invite-only/i); + expect(inviteOnlyMessage).toBeInTheDocument(); + }); + }); + + describe('with paid plans', () => { + test('allows paid signups', async () => { + window.location.hash = '#/portal/signup'; + + // Set up a paid-members only site with a free tier + 3 paid tiers + let { + popupFrame + + } = await setup({ + site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'}, + member: null + }); + + expect(popupFrame).toBeInTheDocument(); + + const emailInput = within(popupFrame.contentDocument).getByLabelText(/email/i); + const nameInput = within(popupFrame.contentDocument).getByLabelText(/name/i); + const chooseBtns = within(popupFrame.contentDocument).queryAllByRole('button', {name: 'Choose'}); + + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + + // There should be 3 choose buttons, one for each paid tier + expect(chooseBtns).toHaveLength(3); + }); + }); + }); + }); + + describe('#/share', () => { + test('opens portal share page', async () => { + window.location.hash = '#/share'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const shareTitle = within(popupFrame.contentDocument).queryByText(/^Share$/i); + expect(shareTitle).toBeInTheDocument(); + const poweredBy = within(popupFrame.contentDocument).queryByText(/Powered by Ghost/i); + expect(poweredBy).not.toBeInTheDocument(); + }); + }); + + describe('#/portal/signup/free', () => { + test('opens free signup page and completes signup even if free plan is hidden', async () => { + window.location.hash = '#/portal/signup/free'; + let { + ghostApi, popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.multipleTiers.onlyPaidPlans, + member: null + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).getByLabelText(/email/i); + const nameInput = within(popupIframeDocument).getByLabelText(/name/i); + const submitButton = within(popupIframeDocument).getByRole('button', {name: 'Sign up'}); + const signinButton = within(popupIframeDocument).getByRole('button', {name: 'Sign in'}); + expect(popupFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + const signupTitle = within(popupFrame.contentDocument).queryByText(/already a member/i); + expect(signupTitle).toBeInTheDocument(); + + // Fill out and submit the signup form + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + + fireEvent.click(submitButton); + + // Verify success message is shown + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + // Verify the API was called with correct parameters + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: 'Jamie Larsen', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + describe('on a paid-members only site', () => { + test('renders paid-members only message and does not allow signups', async () => { + window.location.hash = '#/portal/signup/free'; + let { + popupFrame + } = await setup({ + site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'}, + member: null + }); + + expect(popupFrame).toBeInTheDocument(); + + const paidMembersOnlyMessage = within(popupFrame.contentDocument).queryByText(/This site only accepts paid members/i); + expect(paidMembersOnlyMessage).toBeInTheDocument(); + }); + }); + }); + + describe('#/portal/account', () => { + test('opens portal account home page', async () => { + window.location.hash = '#/portal/account'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountHomeTitle = within(popupFrame.contentDocument).queryByText(/your account/i); + expect(accountHomeTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/account/plans', () => { + test('opens portal account plan page', async () => { + window.location.hash = '#/portal/account/plans'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountPlanTitle = within(popupFrame.contentDocument).queryByText(/choose a plan/i); + expect(accountPlanTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/account/profile', () => { + test('opens portal account profile page', async () => { + window.location.hash = '#/portal/account/profile'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const accountProfileTitle = within(popupFrame.contentDocument).queryByText(/account settings/i); + expect(accountProfileTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/account/newsletter/help', () => { + test('opens portal newsletter receiving help page', async () => { + window.location.hash = '#/portal/account/newsletters/help'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const helpPageTitle = within(popupFrame.contentDocument).queryByText(/help! i'm not receiving emails/i); + expect(helpPageTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/account/newsletter/disabled', () => { + test('opens portal newsletter receiving help page', async () => { + window.location.hash = '#/portal/account/newsletters/disabled'; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + const helpPageTitle = within(popupFrame.contentDocument).queryByText(/why has my email been disabled/i); + expect(helpPageTitle).toBeInTheDocument(); + }); + }); + + describe('#/portal/gift', () => { + test('opens gift page when giftSubscriptions labs flag is enabled', async () => { + window.location.hash = '#/portal/gift'; + + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + const giftSubtitle = within(popupFrame.contentDocument).queryByText(/give the gift of a membership/i); + expect(giftSubtitle).toBeInTheDocument(); + }); + + test('does not open when giftSubscriptions labs flag is disabled', async () => { + window.location.hash = '#/portal/gift'; + + let { + popupFrame, triggerButtonFrame + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {}}, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(popupFrame).not.toBeInTheDocument(); + }); + }); + + describe('#/portal/gift/redeem/', () => { + const giftRedemptionHash = '#/portal/gift/redeem/gift-token-123'; + + const setupGiftRedemption = async ({giftError = null, giftResponse = defaultGiftResponse} = {}) => { + window.location.hash = giftRedemptionHash; + + return setup({ + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + member: FixtureMember.free, + showPopup: false, + giftError, + giftResponse + }); + }; + + const expectGiftRedemptionErrorToast = async ({utils, subtitle}) => { + const notificationFrame = await utils.findByTitle(/portal-notification/i); + expect(notificationFrame).toBeInTheDocument(); + expect(utils.queryByTitle(/portal-popup/i)).not.toBeInTheDocument(); + + const notificationIframeDocument = notificationFrame.contentDocument; + expect(await within(notificationIframeDocument).findByText(/Gift could not be redeemed/i)).toBeInTheDocument(); + expect(within(notificationIframeDocument).queryByText(subtitle)).toBeInTheDocument(); + }; + + test('renders a toast error when gift has expired', async () => { + let { + ghostApi, triggerButtonFrame, ...utils + } = await setupGiftRedemption({ + giftError: new Error('This gift has expired.') + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + + await expectGiftRedemptionErrorToast({ + utils, + subtitle: /This gift has expired\./i + }); + }); + + test('renders a toast error when gift has already been redeemed', async () => { + let { + ghostApi, triggerButtonFrame, ...utils + } = await setupGiftRedemption({ + giftError: new Error('This gift has already been redeemed.') + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + + await expectGiftRedemptionErrorToast({ + utils, + subtitle: /This gift has already been redeemed\./i + }); + }); + + test('renders a toast error when logged-in member already has an active subscription', async () => { + let { + ghostApi, triggerButtonFrame, ...utils + } = await setupGiftRedemption({ + giftError: new Error('You already have an active subscription.') + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + + await expectGiftRedemptionErrorToast({ + utils, + subtitle: /You already have an active subscription\./i + }); + }); + + test('renders a toast error when gift link is invalid', async () => { + let { + ghostApi, triggerButtonFrame, ...utils + } = await setupGiftRedemption({ + giftError: new Error('Failed to load gift data') + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + + await expectGiftRedemptionErrorToast({ + utils, + subtitle: /Gift link is not valid/i + }); + }); + + test('renders gift redemption popup without name/email inputs for a logged-in free member', async () => { + let { + ghostApi, popupFrame, triggerButtonFrame, ...utils + } = await setupGiftRedemption(); + + expect(triggerButtonFrame).toBeInTheDocument(); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + const popupIframeDocument = popupFrame.contentDocument; + expect(await within(popupIframeDocument).findByText(/You've been gifted a membership/i)).toBeInTheDocument(); + expect(within(popupIframeDocument).queryByText(/Bronze/i)).toBeInTheDocument(); + expect(within(popupIframeDocument).queryByText(/1 year/i)).toBeInTheDocument(); + expect(within(popupIframeDocument).queryByText(/Five great stories to read every day/i)).toBeInTheDocument(); + expect(within(popupIframeDocument).queryByLabelText(/your name/i)).not.toBeInTheDocument(); + expect(within(popupIframeDocument).queryByLabelText(/your email/i)).not.toBeInTheDocument(); + expect(popupIframeDocument.querySelector('.gh-gift-redemption-form')).not.toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + }); + + test('renders name/email inputs for an anonymous visitor', async () => { + window.location.hash = giftRedemptionHash; + + let { + ghostApi, popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + member: null, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + const popupIframeDocument = popupFrame.contentDocument; + expect(within(popupIframeDocument).getByLabelText(/your name/i)).toBeInTheDocument(); + expect(within(popupIframeDocument).getByLabelText(/your email/i)).toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).toHaveBeenCalledWith({token: 'gift-token-123'}); + }); + + // TODO for GA: Remove test + test('does not open when giftSubscriptions labs flag is disabled', async () => { + window.location.hash = '#/portal/gift/redeem/gift-token-123'; + + let { + ghostApi, popupFrame, triggerButtonFrame + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {}}, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(popupFrame).not.toBeInTheDocument(); + expect(ghostApi.gift.fetchRedemptionData).not.toHaveBeenCalled(); + }); + }); + + describe('?stripe=gift-purchase-success', () => { + test('opens gift success page when giftSubscriptions labs flag is enabled', async () => { + window.location.href = 'https://portal.localhost/?stripe=gift-purchase-success&gift_token=abc123'; + window.location.search = '?stripe=gift-purchase-success&gift_token=abc123'; + window.location.hash = ''; + window.location.pathname = '/'; + + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + const giftTitle = within(popupFrame.contentDocument).queryByText(/gift ready to share/i); + expect(giftTitle).toBeInTheDocument(); + + const redeemUrl = within(popupFrame.contentDocument).queryByText(/\/gift\/abc123$/); + expect(redeemUrl).toBeInTheDocument(); + }); + + test('does not open gift success page when gift_token is missing', async () => { + window.location.href = 'https://portal.localhost/?stripe=gift-purchase-success'; + window.location.search = '?stripe=gift-purchase-success'; + window.location.hash = ''; + window.location.pathname = '/'; + + let { + popupFrame, triggerButtonFrame + } = await setup({ + site: {...FixtureSite.singleTier.basic, labs: {giftSubscriptions: true}}, + showPopup: false + }); + + expect(triggerButtonFrame).toBeInTheDocument(); + expect(popupFrame).not.toBeInTheDocument(); + }); + }); + + describe('unauthenticated account page access', () => { + test.each([ + {path: 'account', label: 'account'}, + {path: 'account/plans', label: 'account/plans'}, + {path: 'account/profile', label: 'account/profile'}, + {path: 'account/newsletters', label: 'account/newsletters'} + ])('#/portal/$label redirects to signin with redirect URL when not logged in', async ({path}) => { + window.location.hash = `#/portal/${path}`; + let { + ghostApi, popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: null, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + // Should show signin page instead of account page + const popupIframeDocument = popupFrame.contentDocument; + const signinTitle = within(popupIframeDocument).queryByText(/sign in/i); + expect(signinTitle).toBeInTheDocument(); + + // Fill in email and submit to verify the redirect URL is passed through + const emailInput = within(popupIframeDocument).getByLabelText(/email/i); + const submitButton = within(popupIframeDocument).getByRole('button', {name: 'Continue'}); + fireEvent.change(emailInput, {target: {value: 'test@example.com'}}); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'test@example.com', + emailType: 'signin', + redirect: `https://portal.localhost#/portal/${path}/` + }) + ); + }); + }); + }); + + describe('hashchange account page access', () => { + test.each([ + {path: 'account', expectedText: /your account/i}, + {path: 'account/plans', expectedText: /choose a plan/i}, + {path: 'account/profile', expectedText: /account settings/i} + ])('#/portal/$path opens account page via hashchange when logged in', async ({path, expectedText}) => { + // Start with no hash — simulates an already-loaded page + window.location.hash = ''; + let { + popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + + // Navigate via hash change (e.g. clicking ) + window.location.hash = `#/portal/${path}`; + window.dispatchEvent(new HashChangeEvent('hashchange')); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + const pageTitle = within(popupFrame.contentDocument).queryByText(expectedText); + expect(pageTitle).toBeInTheDocument(); + }); + + test.each([ + {path: 'account', label: 'account'}, + {path: 'account/plans', label: 'account/plans'}, + {path: 'account/profile', label: 'account/profile'}, + {path: 'account/newsletters', label: 'account/newsletters'} + ])('#/portal/$label redirects to signin via hashchange when not logged in', async ({path}) => { + // Start with no hash — simulates an already-loaded page + window.location.hash = ''; + let { + ghostApi, popupFrame, triggerButtonFrame, ...utils + } = await setup({ + site: FixtureSite.singleTier.basic, + member: null, + showPopup: false + }); + expect(triggerButtonFrame).toBeInTheDocument(); + + // Now navigate via hash change (e.g. clicking ) + window.location.hash = `#/portal/${path}`; + window.dispatchEvent(new HashChangeEvent('hashchange')); + + popupFrame = await utils.findByTitle(/portal-popup/i); + expect(popupFrame).toBeInTheDocument(); + + // Should show signin page instead of account page + const popupIframeDocument = popupFrame.contentDocument; + const signinTitle = within(popupIframeDocument).queryByText(/sign in/i); + expect(signinTitle).toBeInTheDocument(); + + // Fill in email and submit to verify the redirect URL is passed through + const emailInput = within(popupIframeDocument).getByLabelText(/email/i); + const submitButton = within(popupIframeDocument).getByRole('button', {name: 'Continue'}); + fireEvent.change(emailInput, {target: {value: 'test@example.com'}}); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'test@example.com', + emailType: 'signin', + redirect: `https://portal.localhost#/portal/${path}/` + }) + ); + }); + }); + }); +}); diff --git a/apps/portal/test/setup-tests.js b/apps/portal/test/setup-tests.js new file mode 100644 index 0000000..108678e --- /dev/null +++ b/apps/portal/test/setup-tests.js @@ -0,0 +1,12 @@ +import * as matchers from '@testing-library/jest-dom/matchers'; +import {afterEach, expect} from 'vitest'; +import {cleanup} from '@testing-library/react'; +import {fetch} from 'cross-fetch'; + +// eslint-disable-next-line no-undef +globalThis.fetch = fetch; + +// Add the cleanup function for React testing library +afterEach(cleanup); + +expect.extend(matchers); diff --git a/apps/portal/test/signin-flow.test.js b/apps/portal/test/signin-flow.test.js new file mode 100644 index 0000000..7d02055 --- /dev/null +++ b/apps/portal/test/signin-flow.test.js @@ -0,0 +1,666 @@ +import App from '../src/app.js'; +import {fireEvent, appRender, within, waitFor} from './utils/test-utils'; +import {site as FixtureSite} from './utils/test-fixtures'; +import setupGhostApi from '../src/utils/api.js'; + +const OTC_LABEL_REGEX = /Code/i; + +const setup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(async ({email}) => { + if (email.endsWith('@test-inbox-link.example')) { + return { + inboxLinks: { + provider: 'proton', + android: 'https://fake-proton.example/', + desktop: 'https://fake-proton.example/' + } + }; + } else { + return {}; + } + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve('testtoken'); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + ...utils + }; +}; + +const multiTierSetup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve(`testtoken`); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + const freeTierDescription = site.products?.find(p => p.type === 'free')?.description; + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryAllByText(/free$/i); + const freePlanDescription = within(popupIframeDocument).queryAllByText(freeTierDescription); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + freePlanDescription, + ...utils + }; +}; + +const realLocation = window.location; + +// Helper function to verify OTC-enabled API calls +const expectOTCEnabledSendMagicLinkAPICall = (ghostApi, email) => { + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledWith({ + email, + emailType: 'signin', + integrityToken: 'testtoken', + includeOTC: true + }); +}; + +describe('Signin', () => { + describe('on single tier site', () => { + beforeEach(() => { + // Mock window.location + Object.defineProperty(window, 'location', { + value: new URL('https://portal.localhost/#/portal/signin'), + writable: true + }); + }); + afterEach(() => { + window.location = realLocation; + }); + + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton,popupIframeDocument + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + // Mock sendMagicLink to return otc_ref for OTC flow + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve({success: true, otc_ref: 'test-otc-ref-123'}); + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + const description = await within(popupIframeDocument).findByText(/An email has been sent to jamie@example.com/i); + expect(description).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + + test('without name field', async () => { + const {ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton, + popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.withoutName + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + + test('with only free plan', async () => { + let {ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton, + popupIframeDocument} = await setup({ + site: FixtureSite.singleTier.onlyFreePlan + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + + test('with inbox link', async () => { + const { + ghostApi, + emailInput, + popupIframeDocument, + submitButton + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + fireEvent.change(emailInput, {target: {value: 'test@test-inbox-link.example'}}); + + expect(emailInput).toHaveValue('test@test-inbox-link.example'); + fireEvent.click(submitButton); + + const inboxLinkButton = await within(popupIframeDocument).findByRole('link', {name: /open proton mail/i}); + expect(inboxLinkButton).toBeInTheDocument(); + expect(inboxLinkButton).toHaveAttribute('href', 'https://fake-proton.example/'); + expect(inboxLinkButton).toHaveAttribute('target', '_blank'); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'test@test-inbox-link.example'); + }); + }); +}); + +describe('Signin', () => { + afterEach(() => { + window.location = realLocation; + }); + + describe('on multi tier site', () => { + beforeEach(() => { + // Mock window.location + Object.defineProperty(window, 'location', { + value: new URL('https://portal.localhost/#/portal/signin'), + writable: true + }); + }); + + test('with default settings', async () => { + const {ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton, + popupIframeDocument} = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + + test('without name field', async () => { + const {ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton, + popupIframeDocument} = await multiTierSetup({ + site: FixtureSite.multipleTiers.withoutName + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + + test('with only free plan available', async () => { + let {ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, submitButton, + popupIframeDocument} = await multiTierSetup({ + site: FixtureSite.multipleTiers.onlyFreePlan + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + }); + }); + + describe('redirect parameter handling', () => { + afterEach(() => { + window.location = realLocation; + }); + + // Helper function to open location and complete signin flow + async function openLocationAndCompleteSigninFlow() { + const {ghostApi, popupIframeDocument, emailInput, submitButton} = await setup({ + site: FixtureSite.singleTier.basic, + member: null // No member to trigger signin requirement + }); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.click(submitButton); + + const magicLink = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLink).toBeInTheDocument(); + + return {ghostApi, popupIframeDocument}; + } + + test('passes redirect parameter to sendMagicLink when pageData.redirect is set', async () => { + // Mock the window.location to simulate feedback URL that sets redirect + Object.defineProperty(window, 'location', { + value: new URL('https://portal.localhost/#/feedback/12345/1'), + writable: true + }); + + // opens /#/feedback/12345/1 which redirects to /#/signin, + // setting pageData.redirect in the process + const {ghostApi} = await openLocationAndCompleteSigninFlow(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith( + expect.objectContaining({ + // redirect parameter contains original feedback URL not current URL + redirect: expect.stringContaining('#/feedback/12345/1') + }) + ); + }); + + test('redirect parameter is not passed to sendMagicLink when pageData.redirect is not set', async () => { + // Reset location to regular signin URL so there's no explicit setting of pageData.redirect + Object.defineProperty(window, 'location', { + value: new URL('https://portal.localhost/#/portal/signin'), + writable: true + }); + + const {ghostApi} = await openLocationAndCompleteSigninFlow(); + + // Verify redirect is not included in the sendMagicLink call + const lastCall = ghostApi.member.sendMagicLink.mock.calls[ghostApi.member.sendMagicLink.mock.calls.length - 1][0]; + expect(lastCall.redirect).toBeUndefined(); + }); + }); +}); + +describe('OTC Integration Flow', () => { + const locationAssignMock = vi.fn(); + + beforeEach(() => { + const mockLocation = new URL('https://portal.localhost/#/portal/signin'); + mockLocation.assign = locationAssignMock; + Object.defineProperty(window, 'location', { + value: mockLocation, + writable: true + }); + }); + + afterEach(() => { + window.location = realLocation; + vi.restoreAllMocks(); + locationAssignMock.mockReset(); + }); + + const setupOTCFlow = async ({site, otcRef = 'test-otc-ref-123', returnOtcRef = true}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member: null + }); + }); + + // Mock sendMagicLink to return otcRef for OTC flow or fallback + ghostApi.member.sendMagicLink = vi.fn(() => { + return returnOtcRef + ? Promise.resolve({success: true, otc_ref: otcRef}) + : Promise.resolve({success: true}); + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve('testtoken'); + }); + + ghostApi.member.verifyOTC = vi.fn(() => { + return Promise.resolve({ + redirectUrl: 'https://example.com/welcome' + }); + }); + + const utils = appRender( + + ); + + await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + + return { + ghostApi, + popupIframeDocument, + popupFrame, + ...utils + }; + }; + + const submitSigninForm = async (popupIframeDocument, email = 'jamie@example.com') => { + const emailInput = within(popupIframeDocument).getByLabelText(/email/i); + const submitButton = within(popupIframeDocument).getByRole('button', {name: 'Continue'}); + + fireEvent.change(emailInput, {target: {value: email}}); + fireEvent.click(submitButton); + + const magicLinkText = await within(popupIframeDocument).findByText(/Now check your email/i); + expect(magicLinkText).toBeInTheDocument(); + }; + + const submitOTCForm = (popupIframeDocument, code = '123456') => { + const otcInput = within(popupIframeDocument).getByLabelText(OTC_LABEL_REGEX); + const verifyButton = within(popupIframeDocument).getByRole('button', {name: 'Continue'}); + + fireEvent.change(otcInput, {target: {value: code}}); + fireEvent.click(verifyButton); + }; + + test('complete OTC flow from signin to verification', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledTimes(1); + + submitOTCForm(popupIframeDocument, '123456'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'test-otc-ref-123', + integrityToken: 'testtoken', + redirect: undefined + }); + }); + + expect(ghostApi.member.verifyOTC).toHaveBeenCalledTimes(1); + expect(locationAssignMock).toHaveBeenCalledWith('https://example.com/welcome'); + expect(locationAssignMock).toHaveBeenCalledTimes(1); + }); + + test('OTC flow without otcRef falls back to regular magic link', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic, + returnOtcRef: false + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + expect(ghostApi.member.sendMagicLink).toHaveBeenCalledTimes(1); + + const otcInput = within(popupIframeDocument).queryByLabelText(OTC_LABEL_REGEX); + expect(otcInput).not.toBeInTheDocument(); + + const closeButton = within(popupIframeDocument).getByRole('button', {name: 'Close'}); + expect(closeButton).toBeInTheDocument(); + }); + + test('OTC flow on multi-tier site', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.multipleTiers.basic + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + + expectOTCEnabledSendMagicLinkAPICall(ghostApi, 'jamie@example.com'); + + const otcInput = within(popupIframeDocument).getByLabelText(OTC_LABEL_REGEX); + + expect(otcInput).toBeInTheDocument(); + }); + + test('MagicLink description shows submitted email on OTC flow', async () => { + const {popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + + const description = await within(popupIframeDocument).findByText(/An email has been sent to jamie@example.com/i); + expect(description).toBeInTheDocument(); + }); + + test('OTC verification with invalid code shows error', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + // Mock verifyOTC to return validation error + ghostApi.member.verifyOTC.mockRejectedValueOnce(new Error('Invalid verification code')); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + submitOTCForm(popupIframeDocument, '000000'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '000000', + otcRef: 'test-otc-ref-123', + redirect: undefined, + integrityToken: 'testtoken' + }); + }); + + const errorNotification = await within(popupIframeDocument).findByText(/Invalid verification code/i); + expect(errorNotification).toBeInTheDocument(); + }); + + test('OTC verification without redirectUrl shows default error', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + ghostApi.member.verifyOTC.mockResolvedValueOnce({}); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + submitOTCForm(popupIframeDocument, '654321'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '654321', + otcRef: 'test-otc-ref-123', + redirect: undefined, + integrityToken: 'testtoken' + }); + }); + + const errorNotification = await within(popupIframeDocument).findByText(/Failed to verify code/i); + expect(errorNotification).toBeInTheDocument(); + }); + + test('OTC verification with API error shows error message', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + // Mock verifyOTC to throw API error + ghostApi.member.verifyOTC.mockRejectedValueOnce(new Error('Network error')); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + submitOTCForm(popupIframeDocument, '123456'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'test-otc-ref-123', + redirect: undefined, + integrityToken: 'testtoken' + }); + }); + + const errorNotification = await within(popupIframeDocument).findByText(/Failed to verify code, please try again/i); + expect(errorNotification).toBeInTheDocument(); + }); + + describe('OTC redirect parameter handling', () => { + test('passes redirect parameter from pageData to verifyOTC', async () => { + Object.defineProperty(window, 'location', { + value: new URL('https://portal.localhost/#/feedback/12345/1'), + writable: true + }); + + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + submitOTCForm(popupIframeDocument, '123456'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'test-otc-ref-123', + redirect: expect.stringContaining('#/feedback/12345/1'), + integrityToken: 'testtoken' + }); + }); + }); + + test('verifyOTC works without redirect parameter', async () => { + const {ghostApi, popupIframeDocument} = await setupOTCFlow({ + site: FixtureSite.singleTier.basic + }); + + await submitSigninForm(popupIframeDocument, 'jamie@example.com'); + submitOTCForm(popupIframeDocument, '123456'); + + await waitFor(() => { + expect(ghostApi.member.verifyOTC).toHaveBeenCalledWith({ + otc: '123456', + otcRef: 'test-otc-ref-123', + redirect: undefined, + integrityToken: 'testtoken' + }); + }); + }); + }); +}); diff --git a/apps/portal/test/signup-flow.test.js b/apps/portal/test/signup-flow.test.js new file mode 100644 index 0000000..29f32be --- /dev/null +++ b/apps/portal/test/signup-flow.test.js @@ -0,0 +1,969 @@ +import App from '../src/app.js'; +import {fireEvent, appRender, within, waitFor} from './utils/test-utils'; +import {offer as FixtureOffer, site as FixtureSite} from './utils/test-fixtures'; +import setupGhostApi from '../src/utils/api.js'; + +// Simple deep clone function +const deepClone = obj => JSON.parse(JSON.stringify(obj)); + +const offerSetup = async ({site, member = null, offer}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site: deepClone(site), + member: member ? deepClone(member) : null + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve(`testtoken`); + }); + + ghostApi.site.offer = vi.fn(() => { + return Promise.resolve({ + offers: [offer] + }); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const popupFrame = await utils.findByTitle(/portal-popup/i); + const triggerButtonFrame = await utils.queryByTitle(/portal-trigger/i); + const popupIframeDocument = popupFrame.contentDocument; + + let emailInput, nameInput, continueButton, chooseBtns, signinButton, siteTitle, offerName, offerDescription, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle; + + if (popupIframeDocument) { + emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + continueButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + siteTitle = within(popupIframeDocument).queryByText(site.title); + offerName = within(popupIframeDocument).queryByText(offer.display_title); + offerDescription = within(popupIframeDocument).queryByText(offer.display_description); + + freePlanTitle = within(popupIframeDocument).queryByText('Free'); + monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + } + + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton: continueButton, + chooseBtns, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + offerName, + offerDescription, + ...utils + }; +}; + +const setup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site: deepClone(site), + member: member ? deepClone(member) : null + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(async ({email}) => { + if (email.endsWith('@test-inbox-link.example')) { + return { + inboxLinks: { + provider: 'proton', + android: 'https://fake-proton.example/', + desktop: 'https://fake-proton.example/' + } + }; + } else { + return {}; + } + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve(`testtoken`); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame?.contentDocument; + + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + chooseBtns, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + ...utils + }; +}; + +const multiTierSetup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site: deepClone(site), + member: member ? deepClone(member) : null + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.getIntegrityToken = vi.fn(() => { + return Promise.resolve(`testtoken`); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + const freeTierDescription = site.products?.find(p => p.type === 'free')?.description; + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryAllByText(/free$/i); + const freePlanDescription = within(popupIframeDocument).queryAllByText(freeTierDescription); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + freePlanDescription, + chooseBtns, + ...utils + }; +}; + +describe('Signup', () => { + describe('as free member on single tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + const continueButton = within(popupIframeDocument).queryAllByRole('button', {name: 'Continue'}); + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + // expect(fullAccessTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + // expect(submitButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); + expect(continueButton).toHaveLength(1); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(chooseBtns[0]); + + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: 'Jamie Larsen', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + test('with inbox link', async () => { + const { + emailInput, + nameInput, + popupIframeDocument, + chooseBtns + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'test@test-inbox-link.example'}}); + + expect(emailInput).toHaveValue('test@test-inbox-link.example'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(chooseBtns[0]); + + const inboxLinkButton = await within(popupIframeDocument).findByRole('link', {name: /open proton mail/i}); + expect(inboxLinkButton).toBeInTheDocument(); + expect(inboxLinkButton).toHaveAttribute('href', 'https://fake-proton.example/'); + expect(inboxLinkButton).toHaveAttribute('target', '_blank'); + }); + + test('hides inbox links on iOS', async () => { + const userAgentSpy = vi.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue( + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' + ); + + try { + const { + emailInput, + nameInput, + popupIframeDocument, + chooseBtns + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'test@test-inbox-link.example'}}); + + expect(emailInput).toHaveValue('test@test-inbox-link.example'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(chooseBtns[0]); + + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + const inboxLinkButton = within(popupIframeDocument).queryByRole('link', {name: /open proton mail/i}); + expect(inboxLinkButton).not.toBeInTheDocument(); + } finally { + userAgentSpy.mockRestore(); + } + }); + + test('without name field', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns + } = await setup({ + site: FixtureSite.singleTier.withoutName + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + // expect(fullAccessTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(chooseBtns[0]); + + // Check if magic link page is shown + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: '', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + test('with only free plan', async () => { + let { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + } = await setup({ + site: FixtureSite.singleTier.onlyFreePlan + }); + + const freeProduct = FixtureSite.singleTier.onlyFreePlan.products.find(p => p.type === 'free'); + const benefitText = freeProduct.benefits[0].name; + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(monthlyPlanTitle).not.toBeInTheDocument(); + expect(yearlyPlanTitle).not.toBeInTheDocument(); + expect(fullAccessTitle).not.toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + + // Free tier title, description and benefits should render + expect(freePlanTitle).toBeInTheDocument(); + await within(popupIframeDocument).findByText(freeProduct.description); + await within(popupIframeDocument).findByText(benefitText); + + submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign up'}); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + + // Check if magic link page is shown + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: 'Jamie Larsen', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + }); + + describe('as paid member on single tier site', () => { + test('with default settings on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, submitButton + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); + + const monthlyPlanContainer = within(popupIframeDocument).queryByText(/Monthly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const benefitText = singleTierProduct.benefits[0].name; + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(monthlyPlanContainer.parentNode); + // Wait for the benefit to appear in the UI - it may appear multiple times, so use findAllByText + await waitFor(() => { + expect( + within(popupIframeDocument).queryAllByText(benefitText).length + ).toBeGreaterThan(0); + }); + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('with default settings on yearly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, submitButton, siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle + } = await setup({ + site: FixtureSite.singleTier.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); + + const yearlyPlanContainer = within(popupIframeDocument).queryByText(/Yearly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const benefitText = singleTierProduct.benefits[0].name; + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(yearlyPlanContainer.parentNode); + // Wait for the benefit to appear in the UI - it may appear multiple times, so use findAllByText + await waitFor(() => { + expect( + within(popupIframeDocument).queryAllByText(benefitText).length + ).toBeGreaterThan(0); + }); + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('without name field on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, submitButton + } = await setup({ + site: FixtureSite.singleTier.withoutName + }); + + const monthlyPlanContainer = within(popupIframeDocument).queryByText(/Monthly$/); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + const benefitText = singleTierProduct.benefits[0].name; + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(freePlanTitle).toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(1); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + fireEvent.click(monthlyPlanContainer); + // Wait for the benefit to appear in the UI - it may appear multiple times, so use findAllByText + await waitFor(() => { + expect( + within(popupIframeDocument).queryAllByText(benefitText).length + ).toBeGreaterThan(0); + }); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: '', + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'month' + }); + }); + + test('with only paid plans available', async () => { + let { + ghostApi, popupFrame, popupIframeDocument, triggerButtonFrame, emailInput, nameInput, signinButton, + siteTitle, freePlanTitle, monthlyPlanTitle, yearlyPlanTitle + } = await setup({ + site: FixtureSite.singleTier.onlyPaidPlan + }); + const submitButton = within(popupIframeDocument).queryAllByRole('button', {name: 'Continue'}); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle).not.toBeInTheDocument(); + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toHaveLength(1); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + + fireEvent.click(submitButton[0]); + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.singleTier.basic, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId, + plan: planId, + tierId: tier.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + + test('to an offer via link with portal disabled', async () => { + let site = { + ...FixtureSite.singleTier.basic, + portal_button: false + }; + window.location.hash = `#/portal/offers/${FixtureOffer.id}`; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).not.toBeInTheDocument(); + expect(siteTitle).not.toBeInTheDocument(); + expect(emailInput).not.toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + expect(offerName).not.toBeInTheDocument(); + expect(offerDescription).not.toBeInTheDocument(); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: undefined, + name: undefined, + offerId: offerId, + plan: planId, + tierId: tier.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); + + describe('as free member on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle[0]).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(4); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(chooseBtns[0]); + + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: 'Jamie Larsen', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + test('without name field', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.withoutName + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(freePlanTitle[0]).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(chooseBtns[0]); + + // Check if magic link page is shown + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: '', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + test('with only free plan available', async () => { + let { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, popupIframeDocument, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.onlyFreePlan + }); + + const freeProduct = FixtureSite.multipleTiers.onlyFreePlan.products.find(p => p.type === 'free'); + const benefitText = freeProduct.benefits[0].name; + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + + // Free tier title, description and benefits should render + expect(freePlanTitle.length).toBe(1); + await within(popupIframeDocument).findByText(freeProduct.description); + await within(popupIframeDocument).findByText(benefitText); + + submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign up'}); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + fireEvent.click(submitButton); + + // Check if magic link page is shown + const magicLink = await within(popupIframeDocument).findByText(/now check your email/i); + expect(magicLink).toBeInTheDocument(); + + expect(ghostApi.member.sendMagicLink).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + emailType: 'signup', + name: 'Jamie Larsen', + plan: 'free', + integrityToken: 'testtoken' + }); + }); + + test('should not show free plan if it is hidden', async () => { + let { + popupFrame, triggerButtonFrame, emailInput, nameInput, + siteTitle, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.onlyPaidPlans + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle.length).toBe(0); + }); + }); + + describe('as paid member on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, chooseBtns, + siteTitle, popupIframeDocument, freePlanTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic + }); + + const firstPaidTier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + const regex = new RegExp(`${firstPaidTier.name}$`); + const tierContainer = within(popupIframeDocument).queryAllByText(regex); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(freePlanTitle[0]).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(chooseBtns).toHaveLength(4); + + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + expect(nameInput).toHaveValue('Jamie Larsen'); + + fireEvent.click(tierContainer[0]); + const labelText = popupIframeDocument.querySelector('.gh-portal-discount-label'); + await waitFor(() => { + expect(labelText).toBeInTheDocument(); + }); + + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + fireEvent.click(chooseBtns[1]); + await waitFor(() => expect(ghostApi.member.checkoutPlan).toHaveBeenCalledTimes(1)); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.multipleTiers.basic, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let tier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + fireEvent.change(emailInput, {target: {value: 'jamie@example.com'}}); + fireEvent.change(nameInput, {target: {value: 'Jamie Larsen'}}); + + expect(emailInput).toHaveValue('jamie@example.com'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jamie@example.com', + name: 'Jamie Larsen', + offerId, + plan: planId, + tierId: tier.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + + test('to an offer via link with portal disabled', async () => { + let site = { + ...FixtureSite.multipleTiers.basic, + portal_button: false + }; + window.location.hash = `#/portal/offers/${FixtureOffer.id}`; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site, + offer: FixtureOffer + }); + const singleTier = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).not.toBeInTheDocument(); + expect(siteTitle).not.toBeInTheDocument(); + expect(emailInput).not.toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + expect(offerName).not.toBeInTheDocument(); + expect(offerDescription).not.toBeInTheDocument(); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: undefined, + name: undefined, + offerId: offerId, + plan: planId, + tierId: singleTier.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); + + describe('on a paid-members only site', () => { + describe('with only a free plan', () => { + test('the trigger button redirects to signin instead of signup', async () => { + let { + popupFrame, emailInput, + freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle + } = await setup({ + site: {...FixtureSite.singleTier.onlyFreePlan, members_signup_access: 'paid'} + }); + + expect(popupFrame).toBeInTheDocument(); + + // Check that the signup form is not rendered + // - No tiers + // - No submit button + expect(freePlanTitle).not.toBeInTheDocument(); + expect(monthlyPlanTitle).not.toBeInTheDocument(); + expect(yearlyPlanTitle).not.toBeInTheDocument(); + expect(fullAccessTitle).not.toBeInTheDocument(); + + // Check that the signin form is rendered instead + const signinTitle = within(popupFrame.contentDocument).queryByText(/Sign in/i); + expect(signinTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + }); + }); + + test('does not render the free tier, only paid tiers', async () => { + // Setup paid-members only site with 4 tiers: free + 3 paid + let { + popupFrame, emailInput, nameInput, + freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns + } = await setup({ + site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'} + }); + + expect(popupFrame).toBeInTheDocument(); + + // The free tier should not render, as the site is set to paid-members only + expect(freePlanTitle).not.toBeInTheDocument('Free'); + + // Paid tiers should render + expect(monthlyPlanTitle).toBeInTheDocument(); + expect(yearlyPlanTitle).toBeInTheDocument(); + + // The signup form should render + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + + // There should be three paid tiers to choose from + expect(chooseBtns).toHaveLength(3); + }); + }); +}); diff --git a/apps/portal/test/upgrade-flow.test.js b/apps/portal/test/upgrade-flow.test.js new file mode 100644 index 0000000..4f50eb5 --- /dev/null +++ b/apps/portal/test/upgrade-flow.test.js @@ -0,0 +1,761 @@ +import App from '../src/app.js'; +import {fireEvent, appRender, within} from './utils/test-utils'; +import {offer as FixtureOffer, site as FixtureSite, member as FixtureMember} from './utils/test-fixtures'; +import setupGhostApi from '../src/utils/api.js'; + +const offerSetup = async ({site, member = null, offer}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.site.offer = vi.fn(() => { + return Promise.resolve({ + offers: [offer] + }); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const popupFrame = await utils.findByTitle(/portal-popup/i); + const triggerButtonFrame = utils.queryByTitle(/portal-trigger/i); + const popupIframeDocument = popupFrame.contentDocument; + + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + + const offerName = within(popupIframeDocument).queryByText(offer.display_title); + + const offerDescription = within(popupIframeDocument).queryByText(offer.display_description); + + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + chooseBtns, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + offerName, + offerDescription, + ...utils + }; +}; + +const setup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryByText('Free'); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + const accountHomeTitle = within(popupIframeDocument).queryByText('Your account'); + const viewPlansButton = within(popupIframeDocument).queryByRole('button', {name: 'View plans'}); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + accountHomeTitle, + viewPlansButton, + ...utils + }; +}; + +const multiTierSetup = async ({site, member = null}) => { + const ghostApi = setupGhostApi({siteUrl: 'https://example.com'}); + ghostApi.init = vi.fn(() => { + return Promise.resolve({ + site, + member + }); + }); + + ghostApi.member.sendMagicLink = vi.fn(() => { + return Promise.resolve('success'); + }); + + ghostApi.member.checkoutPlan = vi.fn(() => { + return Promise.resolve(); + }); + + const utils = appRender( + + ); + const freeTierDescription = site.products?.find(p => p.type === 'free')?.description; + const triggerButtonFrame = await utils.findByTitle(/portal-trigger/i); + const popupFrame = utils.queryByTitle(/portal-popup/i); + const popupIframeDocument = popupFrame.contentDocument; + const emailInput = within(popupIframeDocument).queryByLabelText(/email/i); + const nameInput = within(popupIframeDocument).queryByLabelText(/name/i); + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + const signinButton = within(popupIframeDocument).queryByRole('button', {name: 'Sign in'}); + const siteTitle = within(popupIframeDocument).queryByText(site.title); + const freePlanTitle = within(popupIframeDocument).queryAllByText(/free$/i); + const freePlanDescription = within(popupIframeDocument).queryAllByText(freeTierDescription); + const monthlyPlanTitle = within(popupIframeDocument).queryByText('Monthly'); + const yearlyPlanTitle = within(popupIframeDocument).queryByText('Yearly'); + const fullAccessTitle = within(popupIframeDocument).queryByText('Full access'); + const accountHomeTitle = within(popupIframeDocument).queryByText('Your account'); + const viewPlansButton = within(popupIframeDocument).queryByRole('button', {name: 'View plans'}); + return { + ghostApi, + popupIframeDocument, + popupFrame, + triggerButtonFrame, + siteTitle, + emailInput, + nameInput, + signinButton, + submitButton, + freePlanTitle, + monthlyPlanTitle, + yearlyPlanTitle, + fullAccessTitle, + freePlanDescription, + accountHomeTitle, + viewPlansButton, + ...utils + }; +}; + +describe('Logged-in free member', () => { + describe('can upgrade on single tier site', () => { + test('with default settings on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + const monthlyPlanContainer = await within(popupIframeDocument).findByText('Monthly'); + fireEvent.click(monthlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'month' + }); + }); + + test('with default settings on yearly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + await within(popupIframeDocument).findByText('Monthly'); + const yearlyPlanContainer = await within(popupIframeDocument).findByText('Yearly'); + fireEvent.click(yearlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.altFree, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + + test('to an offer via link with portal disabled', async () => { + let site = { + ...FixtureSite.singleTier.basic, + portal_button: false + }; + + window.location.hash = `#/portal/offers/${FixtureOffer.id}`; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: site, + member: FixtureMember.altFree, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).not.toBeInTheDocument(); + expect(siteTitle).not.toBeInTheDocument(); + expect(emailInput).not.toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + expect(offerName).not.toBeInTheDocument(); + expect(offerDescription).not.toBeInTheDocument(); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); + + describe('can upgrade on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle, viewPlansButton + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.free + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + expect(viewPlansButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(viewPlansButton); + await within(popupIframeDocument).findByText('Monthly'); + + // allow default checkbox switch to yearly + await new Promise((r) => { + setTimeout(r, 10); + }); + + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + + fireEvent.click(chooseBtns[0]); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.altFree, + offer: FixtureOffer + }); + let planId = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); +}); + +describe('Logged-in complimentary member', () => { + describe('can upgrade on single tier site', () => { + test('with default settings on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.complimentary + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + + // Complimentary members see "Change" button instead of "View plans" + const changePlanButton = within(popupIframeDocument).queryByRole('button', {name: 'Change'}); + expect(changePlanButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(changePlanButton); + const monthlyPlanContainer = await within(popupIframeDocument).findByText('Monthly'); + fireEvent.click(monthlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'month' + }); + }); + + test('with default settings on yearly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.complimentary + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + + // Complimentary members see "Change" button instead of "View plans" + const changePlanButton = within(popupIframeDocument).queryByRole('button', {name: 'Change'}); + expect(changePlanButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(changePlanButton); + await within(popupIframeDocument).findByText('Monthly'); + const yearlyPlanContainer = await within(popupIframeDocument).findByText('Yearly'); + fireEvent.click(yearlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('with cancelled subscription on monthly plan', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle + } = await setup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.complimentaryWithCancelledSubscription + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + + // Complimentary members see "Change" button instead of "View plans" + const changePlanButton = within(popupIframeDocument).queryByRole('button', {name: 'Change'}); + expect(changePlanButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(changePlanButton); + const monthlyPlanContainer = await within(popupIframeDocument).findByText('Monthly'); + fireEvent.click(monthlyPlanContainer); + // added fake timeout for react state delay in setting plan + await new Promise((r) => { + setTimeout(r, 10); + }); + + const submitButton = within(popupIframeDocument).queryByRole('button', {name: 'Continue'}); + + fireEvent.click(submitButton); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.monthlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'month' + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.singleTier.basic, + member: FixtureMember.altComplimentary, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + + test('to an offer via link with portal disabled', async () => { + let site = { + ...FixtureSite.singleTier.basic, + portal_button: false + }; + + window.location.hash = `#/portal/offers/${FixtureOffer.id}`; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: site, + member: FixtureMember.altComplimentary, + offer: FixtureOffer + }); + let planId = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.singleTier.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).not.toBeInTheDocument(); + expect(siteTitle).not.toBeInTheDocument(); + expect(emailInput).not.toBeInTheDocument(); + expect(nameInput).not.toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).not.toBeInTheDocument(); + expect(offerName).not.toBeInTheDocument(); + expect(offerDescription).not.toBeInTheDocument(); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); + + describe('can upgrade on multi tier site', () => { + test('with default settings', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.complimentary + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + + // Complimentary members see "Change" button instead of "View plans" + const changePlanButton = within(popupIframeDocument).queryByRole('button', {name: 'Change'}); + expect(changePlanButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(changePlanButton); + await within(popupIframeDocument).findByText('Monthly'); + + // allow default checkbox switch to yearly + await new Promise((r) => { + setTimeout(r, 10); + }); + + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + + fireEvent.click(chooseBtns[0]); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('with cancelled subscription', async () => { + const { + ghostApi, popupFrame, triggerButtonFrame, + popupIframeDocument, accountHomeTitle + } = await multiTierSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.complimentaryWithCancelledSubscription + }); + + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(accountHomeTitle).toBeInTheDocument(); + + // Complimentary members see "Change" button instead of "View plans" + const changePlanButton = within(popupIframeDocument).queryByRole('button', {name: 'Change'}); + expect(changePlanButton).toBeInTheDocument(); + + const singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + + fireEvent.click(changePlanButton); + await within(popupIframeDocument).findByText('Monthly'); + + // allow default checkbox switch to yearly + await new Promise((r) => { + setTimeout(r, 10); + }); + + const chooseBtns = within(popupIframeDocument).queryAllByRole('button', {name: 'Choose'}); + + fireEvent.click(chooseBtns[0]); + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + metadata: { + checkoutType: 'upgrade' + }, + offerId: undefined, + plan: singleTierProduct.yearlyPrice.id, + tierId: singleTierProduct.id, + cadence: 'year' + }); + }); + + test('to an offer via link', async () => { + window.location.hash = '#/portal/offers/61fa22bd0cbecc7d423d20b3'; + const { + ghostApi, popupFrame, triggerButtonFrame, emailInput, nameInput, signinButton, submitButton, + siteTitle, + offerName, offerDescription + } = await offerSetup({ + site: FixtureSite.multipleTiers.basic, + member: FixtureMember.altComplimentary, + offer: FixtureOffer + }); + let planId = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid').monthlyPrice.id; + let singleTierProduct = FixtureSite.multipleTiers.basic.products.find(p => p.type === 'paid'); + let offerId = FixtureOffer.id; + expect(popupFrame).toBeInTheDocument(); + expect(triggerButtonFrame).toBeInTheDocument(); + expect(siteTitle).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(nameInput).toBeInTheDocument(); + expect(signinButton).not.toBeInTheDocument(); + expect(submitButton).toBeInTheDocument(); + expect(offerName).toBeInTheDocument(); + expect(offerDescription).toBeInTheDocument(); + + expect(emailInput).toHaveValue('jimmie@example.com'); + expect(nameInput).toHaveValue('Jimmie Larson'); + fireEvent.click(submitButton); + + expect(ghostApi.member.checkoutPlan).toHaveBeenLastCalledWith({ + email: 'jimmie@example.com', + name: 'Jimmie Larson', + offerId, + plan: planId, + tierId: singleTierProduct.id, + cadence: 'month' + }); + + window.location.hash = ''; + }); + }); +}); + diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json new file mode 100644 index 0000000..bc2987d --- /dev/null +++ b/apps/portal/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["vitest/globals"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/apps/portal/vite.config.mjs b/apps/portal/vite.config.mjs new file mode 100644 index 0000000..7af7c7f --- /dev/null +++ b/apps/portal/vite.config.mjs @@ -0,0 +1,95 @@ +/* eslint-env node */ +import {resolve} from 'path'; +import fs from 'fs/promises'; + +import {defineConfig} from 'vitest/config'; +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import reactPlugin from '@vitejs/plugin-react'; +import svgrPlugin from 'vite-plugin-svgr'; + +import pkg from './package.json'; + +import {SUPPORTED_LOCALES} from '@tryghost/i18n'; + +export default defineConfig((config) => { + const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name; + + return { + logLevel: process.env.CI ? 'info' : 'warn', + clearScreen: false, + define: { + 'process.env.NODE_ENV': JSON.stringify(config.mode), + REACT_APP_VERSION: JSON.stringify(process.env.npm_package_version) + }, + preview: { + host: '0.0.0.0', + allowedHosts: true, // allows domain-name proxies to the preview server + port: 4175, + cors: true + }, + server: { + port: 5368 + }, + plugins: [ + cssInjectedByJsPlugin(), + reactPlugin(), + svgrPlugin() + ], + esbuild: { + loader: 'tsx', + include: [/src\/.*\.[jt]sx?$/, /__mocks__\/.*\.[jt]sx?$/, /test\/.*\.[jt]sx?$/], + exclude: [] + }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'load-js-files-as-jsx', + setup(build) { + build.onLoad({filter: /src\/.*\.js$/}, async args => ({ + loader: 'jsx', + contents: await fs.readFile(args.path, 'utf8') + })); + } + } + ] + } + }, + resolve: { + dedupe: ['@tryghost/debug'] + }, + build: { + outDir: resolve(__dirname, 'umd'), + emptyOutDir: true, + reportCompressedSize: false, + minify: true, + sourcemap: true, + cssCodeSplit: false, + lib: { + entry: resolve(__dirname, 'src/index.js'), + formats: ['umd'], + name: pkg.name, + fileName: format => `${outputFileName}.min.js` + }, + rollupOptions: { + output: { + manualChunks: false + } + }, + commonjsOptions: { + include: [/ghost/, /node_modules/], + dynamicRequireRoot: '../../', + dynamicRequireTargets: SUPPORTED_LOCALES.map(locale => `../../ghost/i18n/locales/${locale}/portal.json`) + } + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './test/setup-tests.js', + testTimeout: 10000, + coverage: { + reporter: ['cobertura', 'text-summary', 'html'] + } + } + }; +}); diff --git a/apps/posts/.eslintrc.cjs b/apps/posts/.eslintrc.cjs new file mode 100644 index 0000000..94ebc77 --- /dev/null +++ b/apps/posts/.eslintrc.cjs @@ -0,0 +1,70 @@ +/* eslint-env node */ +const tailwindCssConfig = `${__dirname}/../admin/src/index.css`; + +module.exports = { + root: true, + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + }, + tailwindcss: { + config: tailwindCssConfig + } + }, + rules: { + // Sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + 'no-restricted-imports': ['error', { + paths: [{ + name: '@tryghost/shade', + message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).' + }] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // TODO: re-enable this (maybe fixed fast refresh?) + 'react-refresh/only-export-components': 'off', + + // Suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + + // TODO: re-enable these if deemed useful + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-function': 'off', + + // Custom react rules + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/enforces-negative-arbitrary-values': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/migration-from-tailwind-2': 'warn', + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': 'error' + } +}; diff --git a/apps/posts/.gitignore b/apps/posts/.gitignore new file mode 100644 index 0000000..0f817cd --- /dev/null +++ b/apps/posts/.gitignore @@ -0,0 +1,4 @@ +dist +types +playwright-report +test-results diff --git a/apps/posts/index.html b/apps/posts/index.html new file mode 100644 index 0000000..26af8c7 --- /dev/null +++ b/apps/posts/index.html @@ -0,0 +1,16 @@ + + + + + + + + Posts + + + +
+ + + + \ No newline at end of file diff --git a/apps/posts/package.json b/apps/posts/package.json new file mode 100644 index 0000000..5c993f8 --- /dev/null +++ b/apps/posts/package.json @@ -0,0 +1,87 @@ +{ + "name": "@tryghost/posts", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TryGhost/Ghost/tree/main/apps/posts" + }, + "author": "Ghost Foundation", + "files": [ + "LICENSE", + "README.md", + "dist/" + ], + "main": "./dist/posts.umd.cjs", + "module": "./dist/posts.js", + "exports": { + ".": { + "import": "./dist/posts.js", + "require": "./dist/posts.umd.cjs" + }, + "./api": "./src/api.ts", + "./members": "./src/views/members/members.tsx" + }, + "private": true, + "scripts": { + "dev": "vite build --watch", + "dev:start": "vite", + "build": "tsc && vite build", + "test:unit": "vitest run test/unit", + "lint": "pnpm run lint:code && pnpm run lint:test", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src", + "lint:code:fix": "eslint --ext .js,.ts,.cjs,.tsx --cache --fix src", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test", + "lint:fix": "eslint --ext .js,.ts,.cjs,.tsx --cache src --fix", + "preview": "vite preview", + "test": "pnpm test:unit --coverage" + }, + "devDependencies": { + "@playwright/test": "1.59.1", + "@tanstack/react-query": "4.36.1", + "@tanstack/react-virtual": "3.13.23", + "@testing-library/jest-dom": "^6", + "@testing-library/react": "14.3.1", + "@types/jest": "29.5.14", + "@types/react": "18.3.28", + "@vitest/coverage-v8": "^1.6.1", + "msw": "2.12.14", + "tailwindcss": "^4.2.2", + "vite": "5.4.21", + "vitest": "1.6.1" + }, + "dependencies": { + "@tryghost/admin-x-framework": "workspace:*", + "@tryghost/nql-lang": "0.6.4", + "@tryghost/shade": "workspace:*", + "i18n-iso-countries": "7.14.0", + "moment": "2.24.0", + "moment-timezone": "0.5.45", + "papaparse": "5.5.3", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-router": "7.14.0", + "sonner": "2.0.7", + "use-debounce": "10.1.1", + "zod": "4.1.12" + }, + "nx": { + "targets": { + "dev": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build" + ] + }, + "test:acceptance": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/posts/playwright.config.mjs b/apps/posts/playwright.config.mjs new file mode 100644 index 0000000..8fa5955 --- /dev/null +++ b/apps/posts/playwright.config.mjs @@ -0,0 +1,3 @@ +import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright'; + +export default adminXPlaywrightConfig(); diff --git a/apps/posts/src/api.ts b/apps/posts/src/api.ts new file mode 100644 index 0000000..39a2d4f --- /dev/null +++ b/apps/posts/src/api.ts @@ -0,0 +1,8 @@ +/** + * Public API for cross-package imports. + * Admin uses these exports instead of reaching into src/ directly. + */ +export {default as PostsAppContextProvider} from './providers/posts-app-context'; +export {routes} from './routes'; +export {parseAllSharedViewsJSON} from './views/members/shared-views'; +export type {SharedView, AllSharedViewsParseResult} from './views/members/shared-views'; diff --git a/apps/posts/src/app.tsx b/apps/posts/src/app.tsx new file mode 100644 index 0000000..e3a9a06 --- /dev/null +++ b/apps/posts/src/app.tsx @@ -0,0 +1,43 @@ +import PostsAppContextProvider, {PostsAppContextType} from './providers/posts-app-context'; +import PostsErrorBoundary from '@components/errors/posts-error-boundary'; +import React from 'react'; +import {APP_ROUTE_PREFIX, routes} from '@src/routes'; +import {BaseAppProps, FrameworkProvider, Outlet, RouterProvider} from '@tryghost/admin-x-framework'; +import {ShadeApp} from '@tryghost/shade/app'; + +interface AppProps extends BaseAppProps { + fromAnalytics?: boolean; +} + +const App: React.FC = ({framework, designSystem, fromAnalytics = false, appSettings}) => { + const appContextValue: PostsAppContextType = { + appSettings, + externalNavigate: (url: string) => { + window.location.href = url; + }, + fromAnalytics + }; + + return ( + + + + + + + + + + + + ); +}; + +export default App; diff --git a/apps/posts/src/index.tsx b/apps/posts/src/index.tsx new file mode 100644 index 0000000..74d98a5 --- /dev/null +++ b/apps/posts/src/index.tsx @@ -0,0 +1,6 @@ +import './styles/index.css'; +import App from './app'; + +export { + App as AdminXApp +}; diff --git a/apps/posts/src/nql-lang.d.ts b/apps/posts/src/nql-lang.d.ts new file mode 100644 index 0000000..077537c --- /dev/null +++ b/apps/posts/src/nql-lang.d.ts @@ -0,0 +1,7 @@ +declare module '@tryghost/nql-lang' { + const nql: { + parse(input: string): unknown; + }; + + export default nql; +} diff --git a/apps/posts/src/routes.tsx b/apps/posts/src/routes.tsx new file mode 100644 index 0000000..3c75882 --- /dev/null +++ b/apps/posts/src/routes.tsx @@ -0,0 +1,80 @@ +import {ErrorPage} from '@tryghost/shade/primitives'; +import {RouteObject, lazyComponent} from '@tryghost/admin-x-framework'; +// import {withFeatureFlag} from '@src/hooks/with-feature-flag'; + +export const APP_ROUTE_PREFIX = '/'; + +// Wrap components with feature flag protection where needed +// e.g. +// const ProtectedNewsletter = withFeatureFlag(Newsletter, 'trafficAnalyticsAlpha', '/analytics/', 'Newsletter'); + +export const routes: RouteObject[] = [ + { + // Root route configuration + path: '', + errorElement: {}} />, // @TODO: add back to dashboard click handle + children: [ + { + // Post Analytics + path: 'posts/analytics/:postId', + lazy: async () => { + const [ + {default: PostAnalyticsProvider}, + {default: PostAnalytics} + ] = await Promise.all([ + import('./providers/post-analytics-context'), + import('./views/PostAnalytics/post-analytics') + ]); + return { + element: ( + + + + ) + }; + }, + children: [ + { + path: '', + lazy: lazyComponent(() => import('@views/PostAnalytics/Overview/overview')) + }, + { + path: 'web', + lazy: lazyComponent(() => import('@views/PostAnalytics/Web/web')) + }, + { + path: 'growth', + lazy: lazyComponent(() => import('@views/PostAnalytics/Growth/growth')) + }, + { + path: 'newsletter', + lazy: lazyComponent(() => import('@views/PostAnalytics/Newsletter/newsletter')) + } + ] + }, + { + path: 'tags', + children: [ + { + index: true, + lazy: lazyComponent(() => import('@views/Tags/tags')) + }, + { + path: ':tagSlug', + element: null + } + ] + }, + { + path: 'comments', + lazy: lazyComponent(() => import('@views/comments/comments')) + }, + + // Error handling + { + path: '*', + element: {}} /> // @TODO: add back to dashboard click handle + } + ] + } +]; diff --git a/apps/posts/src/standalone.tsx b/apps/posts/src/standalone.tsx new file mode 100644 index 0000000..d013a7a --- /dev/null +++ b/apps/posts/src/standalone.tsx @@ -0,0 +1,5 @@ +import './styles/index.css'; +import App from './app'; +import renderShadeApp from '@tryghost/admin-x-framework/test/render-shade'; + +renderShadeApp(App, {}); diff --git a/apps/posts/src/typings.d.ts b/apps/posts/src/typings.d.ts new file mode 100644 index 0000000..f5e5fee --- /dev/null +++ b/apps/posts/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'papaparse'; diff --git a/apps/posts/test/.eslintrc.cjs b/apps/posts/test/.eslintrc.cjs new file mode 100644 index 0000000..109cf04 --- /dev/null +++ b/apps/posts/test/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/ts-test' + ], + rules: { + // We aren't using Mocha so we can disable some Ghost test rules. + 'ghost/mocha/no-mocha-arrows': 'off' + } +}; diff --git a/apps/posts/test/setup.ts b/apps/posts/test/setup.ts new file mode 100644 index 0000000..5313131 --- /dev/null +++ b/apps/posts/test/setup.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom/vitest'; +import {setupShadeMocks} from '@tryghost/admin-x-framework/test/setup'; + +// Set up common mocks for shade components +setupShadeMocks(); \ No newline at end of file diff --git a/apps/posts/tsconfig.declaration.json b/apps/posts/tsconfig.declaration.json new file mode 100644 index 0000000..c7b87e9 --- /dev/null +++ b/apps/posts/tsconfig.declaration.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/posts/tsconfig.json b/apps/posts/tsconfig.json new file mode 100644 index 0000000..6e378be --- /dev/null +++ b/apps/posts/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": "./src", + "paths": { + "@src/*": ["*"], + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@hooks/*": ["hooks/*"], + "@utils/*": ["utils/*"], + "@views/*": ["views/*"] + } + }, + "include": ["src", "test"] +} diff --git a/apps/posts/vite.config.mjs b/apps/posts/vite.config.mjs new file mode 100644 index 0000000..cfada95 --- /dev/null +++ b/apps/posts/vite.config.mjs @@ -0,0 +1,28 @@ +import adminXViteConfig from '@tryghost/admin-x-framework/vite'; +import pkg from './package.json'; +import {resolve} from 'path'; + +export default (function viteConfig() { + return adminXViteConfig({ + packageName: pkg.name, + entry: resolve(__dirname, 'src/index.tsx'), + overrides: { + test: { + include: [ + './test/unit/**/*', + './src/**/*.test.ts' + ] + }, + resolve: { + alias: { + '@src': resolve(__dirname, './src'), + '@assets': resolve(__dirname, './src/assets'), + '@components': resolve(__dirname, './src/components'), + '@hooks': resolve(__dirname, './src/hooks'), + '@utils': resolve(__dirname, './src/utils'), + '@views': resolve(__dirname, './src/views') + } + } + } + }); +}); diff --git a/apps/posts/vitest.config.ts b/apps/posts/vitest.config.ts new file mode 100644 index 0000000..bb9fe1b --- /dev/null +++ b/apps/posts/vitest.config.ts @@ -0,0 +1,3 @@ +import {createVitestConfig} from '@tryghost/admin-x-framework/test/vitest-config'; + +export default createVitestConfig(); diff --git a/apps/shade/.eslintrc.cjs b/apps/shade/.eslintrc.cjs new file mode 100644 index 0000000..ba95ecb --- /dev/null +++ b/apps/shade/.eslintrc.cjs @@ -0,0 +1,50 @@ +const tailwindCssConfig = `${__dirname}/../admin/src/index.css`; + +module.exports = { + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:storybook/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + }, + tailwindcss: { + config: tailwindCssConfig + } + }, + rules: { + // suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + + // Enforce a kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/enforces-negative-arbitrary-values': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/migration-from-tailwind-2': 'warn', + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': 'error' + } +}; diff --git a/apps/shade/.gitignore b/apps/shade/.gitignore new file mode 100644 index 0000000..96a58c8 --- /dev/null +++ b/apps/shade/.gitignore @@ -0,0 +1,6 @@ +es +types +dist +playwright-report +test-results +storybook-static \ No newline at end of file diff --git a/apps/shade/.storybook/Inter.ttf b/apps/shade/.storybook/Inter.ttf new file mode 100644 index 0000000..1cb674b Binary files /dev/null and b/apps/shade/.storybook/Inter.ttf differ diff --git a/apps/shade/.storybook/main.tsx b/apps/shade/.storybook/main.tsx new file mode 100644 index 0000000..04ba13d --- /dev/null +++ b/apps/shade/.storybook/main.tsx @@ -0,0 +1,54 @@ +import {dirname} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import type { StorybookConfig } from "@storybook/react-vite"; +import path from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + + addons: [ + "@storybook/addon-links", + { + name: "@storybook/addon-docs", + options: { + mdxPluginOptions: { + mdxCompileOptions: { + providerImportSource: "@storybook/addon-docs/mdx-react-shim" + } + } + } + } + ], + + framework: { + name: "@storybook/react-vite", + options: {}, + }, + + async viteFinal(config) { + config.resolve = config.resolve ?? {}; + config.build = config.build ?? {}; + config.build.rollupOptions = config.build.rollupOptions ?? {}; + + if (Array.isArray(config.resolve.alias)) { + config.resolve.alias = [ + ...config.resolve.alias, + {find: '@', replacement: path.resolve(__dirname, '../src')} + ]; + } else { + config.resolve.alias = { + ...(config.resolve.alias ?? {}), + '@': path.resolve(__dirname, '../src') + }; + } + + // The package Vite config externalizes node_modules for library builds. + // Storybook needs those modules bundled for docs/stories compilation. + delete config.build.rollupOptions.external; + return config; + } +}; +export default config; diff --git a/apps/shade/.storybook/manager.tsx b/apps/shade/.storybook/manager.tsx new file mode 100644 index 0000000..823499a --- /dev/null +++ b/apps/shade/.storybook/manager.tsx @@ -0,0 +1,6 @@ +import {addons} from 'storybook/manager-api'; +import shadeTheme from './shade-theme'; + +addons.setConfig({ + theme: shadeTheme +}); diff --git a/apps/shade/.storybook/preview.tsx b/apps/shade/.storybook/preview.tsx new file mode 100644 index 0000000..2e3ff36 --- /dev/null +++ b/apps/shade/.storybook/preview.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +import '../styles.css'; +import './storybook.css'; + +import type { Preview } from "@storybook/react-vite"; +import ShadeProvider from '../src/providers/shade-provider'; +import shadeTheme from './shade-theme'; + +const customViewports = { + sm: { + name: 'sm', + styles: { + width: '480px', + height: '801px', + }, + }, + md: { + name: 'md', + styles: { + width: '640px', + height: '801px', + }, + }, + lg: { + name: 'lg', + styles: { + width: '1024px', + height: '801px', + }, + }, + xl: { + name: 'xl', + styles: { + width: '1320px', + height: '801px', + }, + }, + tablet: { + name: 'tablet', + styles: { + width: '860px', + height: '801px', + }, + }, +}; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: { + method: 'alphabetical', + order: [ + 'Primitives', + 'Components', + 'Layout', + 'Features', + 'Experimental', + 'Introduction', + 'Principles', + 'Architecture', + 'Primitives Guide', + 'Component Rules and Guarantees', + 'Tokens', + 'Contributing', + '*' + ], + }, + }, + docs: { + theme: shadeTheme, + }, + viewport: { + viewports: { + ...customViewports, + }, + }, + }, + decorators: [ + (Story, context) => { + let {scheme} = context.globals; + + return ( +
+ {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} + + + +
); + }, + ], + globalTypes: { + scheme: { + name: "Scheme", + description: "Select light or dark mode", + defaultValue: "light", + toolbar: { + icon: "mirror", + items: ["light", "dark"], + dynamicTitle: true + } + } + } +}; + +export default preview; diff --git a/apps/shade/.storybook/shade-theme.tsx b/apps/shade/.storybook/shade-theme.tsx new file mode 100644 index 0000000..5d282ab --- /dev/null +++ b/apps/shade/.storybook/shade-theme.tsx @@ -0,0 +1,37 @@ +import {create} from 'storybook/theming/create'; + +export default create({ + base: 'light', + // Typography + fontBase: '"Inter", sans-serif', + fontCode: 'monospace', + + brandTitle: 'Ghost | Shade', + brandUrl: 'https://ghost.org', + brandTarget: '_self', + + // + colorPrimary: '#30CF43', + colorSecondary: '#15171A', + + // UI + appBg: '#ffffff', + appContentBg: '#ffffff', + appBorderColor: '#EBEEF0', + appBorderRadius: 0, + + // Text colors + textColor: '#15171A', + textInverseColor: '#ffffff', + + // Toolbar default and active colors + barTextColor: '#9E9E9E', + barSelectedColor: '#15171A', + barBg: '#ffffff', + + // Form colors + inputBg: '#ffffff', + inputBorder: '#15171A', + inputTextColor: '#15171A', + inputBorderRadius: 2, +}); diff --git a/apps/shade/.storybook/storybook.css b/apps/shade/.storybook/storybook.css new file mode 100644 index 0000000..65cb79e --- /dev/null +++ b/apps/shade/.storybook/storybook.css @@ -0,0 +1,324 @@ +/* + * We load Inter in Ember admin, so loading it explicitly here makes the final rendering + * in Storybook match the final rendering when embedded in Ember + */ +@font-face { + font-family: "Inter"; + src: url("./Inter.ttf") format("truetype-variations"); + font-weight: 100 900; +} + +:root { + font-size: 62.5%; + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +html, +body, +#root { + width: 100%; + height: 100%; + margin: 0; + letter-spacing: unset; +} + +.sbdocs-wrapper { + padding: 3vmin !important; +} + +.sbdocs-wrapper .sbdocs-content { + max-width: 1320px; +} + +.sb-doc { + max-width: 740px; + width: 100%; + margin: 0 auto !important; +} + +.sb-doc, +.sb-doc a, +.sb-doc h1, +.sb-doc h2, +.sb-doc h3, +.sb-doc h4, +.sb-doc h5, +.sb-doc h6, +.sb-doc p, +.sb-doc ul li, +.sbdocs-title, +.sb-doc ol li { + font-family: Inter, sans-serif !important; + padding: 0 !important; +} + +.sb-doc a { + color: #394047 !important; + text-decoration: underline !important; +} + +.sb-doc a:hover { + opacity: 0.85; +} + +.sb-doc h1 { + font-size: 36px !important; + letter-spacing: -0.04em !important; + margin-bottom: 20px; +} + +.sb-doc h2 { + margin-top: 40px !important; + font-size: 24px; + border: none; + margin-bottom: 2px; + letter-spacing: -0.02em !important; +} + +.sb-doc h3 { + margin-top: 40px !important; + margin-bottom: 4px !important; + font-size: 18px; +} + +.sb-doc h4 { + margin: 0 0 4px !important; +} + +.sb-doc p, +.sb-doc div, +.sb-doc ul li, +.sb-doc ol li { + font-size: 15px; + line-height: 1.5em; +} + +.sb-doc ul li, +.sb-doc ol li { + margin-bottom: 8px; +} + +.sb-doc h2 + p, +.sb-doc h3 + p { + margin-top: 8px; +} + +.sb-doc img, +.sb-wide img { + margin-top: 40px !important; + margin-bottom: 40px !important; +} + +.sb-doc img.small { + max-width: 520px; + margin: 0 auto; + display: block; +} + +.sb-doc p.excerpt { + font-size: 19px; + letter-spacing: -0.02em; +} + +.sb-doc .highlight { + padding: 12px 20px; + border-radius: 4px; + background: #ebeef0; +} + +.sb-doc .highlight.purple { + background: #f0e9fa; +} + +.sb-doc .highlight.purple a { + color: #8e42ff; +} + +/* Welcome */ +.sb-doc img.main-image { + margin-top: -2vmin !important; + margin-left: -44px; + margin-right: -32px; + margin-bottom: 0 !important; + max-width: unset; + width: calc(100% + 64px); +} + +.sb-doc .main-structure-container { + display: flex; + gap: 32px; + margin: 32px 0 80px; +} + +.sb-doc .main-structure-container div { + flex-basis: 33%; +} + +.sb-doc .main-structure-container div p { + display: flex; + flex-direction: column; + gap: 4px; +} + +.sb-doc .main-structure-container img { + margin: 12px 0 !important; + width: 32px; + height: 32px; +} + +.sb-doc .main-structure-container div h4 { + border-bottom: 1px solid #ebeef0; + padding-bottom: 8px !important; + margin-bottom: 8px !important; +} + +.sb-doc .main-structure-container div p { + margin: 0; + font-size: 13.5px; +} + +/* Colors */ +.color-grid { + display: flex; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} + +.color-grid div { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px; + border-radius: 4px; + border: 1px solid #ebeef0; +} + +.color-grid .swatch { + display: block; + background: #efefef; + border-radius: 100%; + width: 28px; + height: 28px; +} + +.swatch.green { + background: #30cf43; +} + +.swatch.black { + background: #15171a; +} + +.swatch.white { + background: #ffffff; + border: 1px solid #ebeef0; +} + +.swatch.lime { + background: #b5ff18; +} +.swatch.blue { + background: #14b8ff; +} +.swatch.purple { + background: #8e42ff; +} +.swatch.pink { + background: #fb2d8d; +} +.swatch.yellow { + background: #ffb41f; +} +.swatch.red { + background: #f50b23; +} + +/* Icons */ + +.sb-doc .streamline { + display: grid; + grid-template-columns: auto 240px; + gap: 32px; +} + +.sbdocs-a { + color: #394047 !important; + text-decoration: underline !important; +} + +.sb-icon-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 16px; + padding: 16px; +} + +.sb-icon { + display: flex; + flex-direction: column; + align-items: center; + justify-items: center; + gap: 4px; + padding: 8px; + border: 1px solid #efefef; + border-radius: 5px; + cursor: pointer; +} + +.sb-icon:hover { + background-color: #f9f9f9; +} + +.prismjs div { + font-size: 13px !important; + /* color: #fff !important; */ +} + +/* .docblock-source { + border-radius: 7px !important; + border: none !important; + box-shadow: none !important; + background: #15171a !important; +} + +.docblock-source div { + background: unset; +} + +.docblock-source button { + background: #394047 !important; + border: none; + color: #fff; +} */ + +.sbdocs a.button { + display: inline-block; + padding: 6px 13px !important; + background: #15171a; + text-decoration: none !important; + font-size: 13px; + border-radius: 5px; + color: #fff !important; + font-weight: 600; +} + +.sbdocs li > ul { + padding-top: 10px; +} + +.sbdocs hr { + margin: 40px 0; +} + +.docs-story .shade { + overflow: unset; +} diff --git a/apps/shade/AGENTS.md b/apps/shade/AGENTS.md new file mode 100644 index 0000000..996c1b4 --- /dev/null +++ b/apps/shade/AGENTS.md @@ -0,0 +1,78 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `src/components/ui/*`: Atomic UI components (Radix/ShadCN-based). Each component should have a `*.stories.tsx` file next to it. +- `src/components/layout/*`: Reusable layout containers (Page, Heading, Header, ViewHeader, ErrorPage). +- `src/components/features/*`: Higher-level, opinionated components (e.g., PostShareModal, SourceTabs). +- `src/hooks/*`: Custom React hooks. +- `src/lib/utils.ts`: Shared utilities (class merging, formatting, chart helpers). +- `src/providers/*` and `src/shade-app.tsx`: Context + app wrapper that scopes styles to `.shade`. +- `src/assets/*`: Logos and custom icon SVGs (icons auto-exported via `Icon`). +- `test/unit/*`: Vitest tests. `test/unit/utils/test-utils.tsx` provides a `render` helper. +- Build artifacts: `es/` (compiled ESM) and `types/` (generated `.d.ts`). Storybook config lives in `.storybook/`. + +## Build, Test, and Development Commands +- `pnpm build` — Type declarations + Vite library build to `es/`. +- `pnpm test` — Type-checks then runs Vitest with coverage. +- `pnpm test:unit` / `pnpm test:types` — Run unit tests or TS type-checks only. +- `pnpm lint` — ESLint for source and tests (`tailwindcss` plugin enabled). +- `pnpm storybook` — Run Storybook locally. `pnpm build-storybook` — static export. + +## Coding Style & Naming Conventions +- React + TypeScript. Prefer composable components over heavy prop configuration. +- Filenames: ShadCN-generated files keep kebab-case; component identifiers use `PascalCase`. +- Functions/vars: `camelCase`. Keep file-scoped components in the same file. +- Always forward and merge `className` with `cn(...)`. Use CVA for variants when useful. +- Tailwind is scoped via `.shade`; dark mode uses `.dark`. Follow ESLint and `tailwindcss/*` rules. + +## Component API Patterns +- Prefer compound subcomponents for multi‑region components (e.g., `Header.Title`, `Header.Meta`, `Header.Actions`) instead of many props. +- Keep parts small and focused; attach them as static properties and export as named exports. +- Expose Radix/HTML props where sensible; always include `className` and merge with `cn(...)`. +- Demonstrate each part in Storybook stories (e.g., “With actions”, “With meta”). + +## Adding New Components +- Prefer ShadCN first: search for an equivalent and add via `npx shadcn@latest add `. Follow the guardrails above to avoid accidental overwrites. +- Location & exports: place new UI components under `src/components/ui` and export them from `src/index.ts`. +- Storybook: add a sibling `*.stories.tsx` file with an overview (what/why) and stories showing different use cases/variants (sizes, states, important props). If you've added a ShadCN component then copy the examples from the ShadCN component documentation at https://ui.shadcn.com/docs/components/[component name]. +- Implementation: forward `className` and merge with `cn(...)`; use CVA for variants where appropriate. +- Verification: `pnpm lint`, `pnpm test`, plus `pnpm storybook` to visually validate stories before opening a PR. +- **Important**: Always run `pnpm lint` after making changes to fix any ESLint errors and warnings before committing. + +## Testing Guidelines (TBD) +We are finalizing a formal testing strategy. + +Interim expectations: +- Use the existing setup (Vitest + Testing Library + jsdom) when adding tests. +- Location/patterns: `test/unit/**/*.(test).(ts|tsx|js)`; use `test/unit/utils/test-utils.tsx`’s `render` when a wrapper is needed. +- For new UI components, prioritize comprehensive Storybook stories; add focused unit tests where they provide real value (e.g., hooks, utils, logic-heavy parts). +- No strict coverage threshold yet; run `pnpm test` locally to ensure the suite passes. + +## Commit & Pull Request Guidelines +- Commit messages are the release notes. Follow this structure: + - 1st line: ≤ 80 chars, past tense, start with one of: Fixed/Changed/Updated/Improved/Added/Removed/Reverted/Moved/Released/Bumped/Cleaned. + - 2nd line: blank. + - 3rd line: magic word + absolute issue URL (e.g., `ref https://linear.app/...` or `closes ...`). + - 4th+: explain the “why”/context behind the change. +- Gotchas: don’t use `ref:` (colon) — magic word must be followed by a space; Linear requires full URLs. + - Dependency bumps: focus the message on resulting user‑visible changes (no need to list which internal packages changed). +- PRs: describe changes, link issues, add screenshots/gifs for UI changes, and update/add stories. + +## Storybook Documentation for New Components +Refer to “Adding New Components” for the process. Story content should: +- Include a short overview (what the component does and primary use case). +- Demonstrate key variants and states (sizes, disabled/loading, critical props). +- Be minimal but representative; prefer CVA variants/props over ad‑hoc class overrides. + - Avoid obvious technical implementation details (e.g., which libraries are used). Prefer one‑line guidance per story explaining when to use that variant/size/state. + +## Notes for Contributors (and Agents) +- Do not rename ShadCN-generated files purely for casing. +- Use the `@` alias for internal imports (e.g., `@/lib/utils`). +- When changing tokens/config, verify Storybook and a library build still succeed. +- When adding new components to Shade, always look for a ShadCN/UI equivalent and if it exists add it using `npx`. + +## ShadCN Component Installation Guardrails +- Never overwrite existing Shade components during `npx shadcn@latest add ` prompts. Choose “No” when asked to overwrite. +- Always work on a fresh branch and commit a clean baseline before running the installer so you can easily revert: `git checkout -b chore/shadcn-add-`. +- If a component already exists in `src/components/ui`, generate the new version in a temporary workspace (scratch repo), then manually diff and port only the desired changes into the existing Shade file. +- After integrating, run `pnpm lint`, `pnpm test`, and verify in Storybook before merging. diff --git a/apps/shade/README.md b/apps/shade/README.md new file mode 100644 index 0000000..752e9f3 --- /dev/null +++ b/apps/shade/README.md @@ -0,0 +1,66 @@ +# Shade + +Ghost Design System that can be used by micro-frontends. + +## Usage + +Shade is consumed internally across Ghost apps. The package is currently private; when published, consumption will follow standard npm usage. + +Example: + +```tsx +import {Button} from '@tryghost/shade/components'; + +export function Example() { + return ; +} +``` + +CSS-first styling contract: + +```css +/* app entry CSS */ +@import "@tryghost/shade/styles.css"; +``` + +No Tailwind preset/config import is required for Shade runtime styling. + +Scoping and dark mode: + +- All styles are scoped under a `.shade` container. +- Dark mode is toggled by adding `.dark` within that scope. + +Wrap your surface with `ShadeApp` (includes provider and scoping): + +```tsx +import {ShadeApp} from '@tryghost/shade/app'; + + + {/* your UI */} + +``` + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +Local docs with Storybook: + +- `pnpm storybook` — run Storybook and view docs under `src/docs/` +- `pnpm build-storybook` — build a static export + +## Test + +- `pnpm test` — type-checks and runs Vitest with coverage +- `pnpm test:unit` — type-checks and runs Vitest +- `pnpm test:types` — TypeScript only +- `pnpm lint` — ESLint for `src/` and `test/` + +## Notes + +- Utilities live at `@/lib/utils` (not `@/utils`). Use `cn(...)` to merge class names and prefer CVA for variants. +- Docs live alongside the code and are rendered via Storybook (`src/docs/*`). diff --git a/apps/shade/components.json b/apps/shade/components.json new file mode 100644 index 0000000..ac585f3 --- /dev/null +++ b/apps/shade/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "css": "styles.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@reui": "https://reui.io/r/{name}.json" + } +} diff --git a/apps/shade/package.json b/apps/shade/package.json new file mode 100644 index 0000000..998cf77 --- /dev/null +++ b/apps/shade/package.json @@ -0,0 +1,180 @@ +{ + "name": "@tryghost/shade", + "type": "module", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/shade", + "author": "Ghost Foundation", + "private": true, + "main": "es/index.js", + "types": "types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "import": "./es/index.js", + "default": "./es/index.js" + }, + "./tokens": { + "types": "./types/tokens.d.ts", + "import": "./es/tokens.js", + "default": "./es/tokens.js" + }, + "./primitives": { + "types": "./types/primitives.d.ts", + "import": "./es/primitives.js", + "default": "./es/primitives.js" + }, + "./components": { + "types": "./types/components.d.ts", + "import": "./es/components.js", + "default": "./es/components.js" + }, + "./patterns": { + "types": "./types/patterns.d.ts", + "import": "./es/patterns.js", + "default": "./es/patterns.js" + }, + "./app": { + "types": "./types/app.d.ts", + "import": "./es/app.js", + "default": "./es/app.js" + }, + "./utils": { + "types": "./types/utils.d.ts", + "import": "./es/utils.js", + "default": "./es/utils.js" + }, + "./styles.css": "./styles.css", + "./tailwind.theme.css": "./tailwind.theme.css", + "./tokens.css": "./tokens.css", + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "dev": "vite build --watch", + "build": "tsc -p tsconfig.declaration.json && vite build", + "test": "pnpm test:types && vitest run --coverage", + "test:unit": "pnpm test:types && vitest run", + "test:types": "tsc --noEmit", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache", + "lint": "pnpm lint:code && pnpm lint:test", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "files": [ + "es", + "types", + "styles.css", + "tailwind.theme.css", + "theme-variables.css", + "tokens.css", + "preflight.css" + ], + "devDependencies": { + "@codemirror/lang-html": "6.4.11", + "@radix-ui/react-tooltip": "1.2.8", + "@storybook/addon-docs": "10.3.3", + "@storybook/addon-links": "10.3.3", + "@storybook/react-vite": "10.3.3", + "@tailwindcss/postcss": "4.2.1", + "@tailwindcss/vite": "4.2.1", + "@testing-library/react": "14.3.1", + "@testing-library/react-hooks": "8.0.1", + "@types/lodash-es": "4.17.12", + "@types/node": "22.19.17", + "@types/react-world-flags": "1.6.0", + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "^1.6.1", + "c8": "10.1.3", + "chai": "4.5.0", + "eslint": "catalog:", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-storybook": "10.3.3", + "eslint-plugin-tailwindcss": "4.0.0-beta.0", + "glob": "^10.5.0", + "jsdom": "28.1.0", + "lodash-es": "4.18.1", + "postcss": "8.5.6", + "rollup-plugin-node-builtins": "2.1.2", + "sinon": "18.0.1", + "storybook": "10.3.3", + "tailwindcss": "4.2.1", + "tw-animate-css": "1.4.0", + "typescript": "5.9.3", + "vite": "5.4.21", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + }, + "dependencies": { + "@dnd-kit/core": "6.3.1", + "@dnd-kit/sortable": "7.0.2", + "@ebay/nice-modal-react": "1.2.13", + "@hookform/resolvers": "5.2.2", + "@number-flow/react": "0.5.10", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-avatar": "1.1.11", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.8", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.8", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.4", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@sentry/react": "7.120.4", + "@types/color": "4.2.1", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@types/validator": "13.15.10", + "@uiw/react-codemirror": "4.25.2", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "color": "^5.0.3", + "lucide-react": "0.577.0", + "moment-timezone": "^0.5.48", + "next-themes": "0.4.6", + "react": "18.3.1", + "react-colorful": "5.6.1", + "react-dom": "18.3.1", + "react-dropzone": "14.2.3", + "react-hook-form": "7.72.1", + "react-hot-toast": "2.6.0", + "react-select": "5.10.2", + "react-world-flags": "1.6.0", + "recharts": "2.15.4", + "sonner": "2.0.7", + "tailwind-merge": "3.5.0", + "validator": "13.12.0", + "zod": "4.1.12" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "nx": { + "targets": { + "build": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/shade/postcss.config.cjs b/apps/shade/postcss.config.cjs new file mode 100644 index 0000000..2bea32c --- /dev/null +++ b/apps/shade/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {} + } +}; diff --git a/apps/shade/preflight.css b/apps/shade/preflight.css new file mode 100644 index 0000000..f90488c --- /dev/null +++ b/apps/shade/preflight.css @@ -0,0 +1,404 @@ +:where(.shade) { + /* + * Neutralize TW v4 default --text-{size}--line-height theme variables. + * In v3, text-* utilities only set font-size. In v4, they reference + * var(--tw-leading, var(--text-{size}--line-height)) which forces a + * line-height from the built-in theme. Setting these to `initial` + * makes them the CSS "guaranteed-invalid value", so the var() chain + * collapses and line-height behaves as `inherit` — matching v3. + */ + --text-xs--line-height: initial; + --text-sm--line-height: initial; + --text-base--line-height: initial; + --text-lg--line-height: initial; + --text-xl--line-height: initial; + --text-2xl--line-height: initial; + --text-3xl--line-height: initial; + --text-4xl--line-height: initial; + + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) + */ + + *, + ::before, + ::after { + box-sizing: border-box; /* 1 */ + max-width: revert; + max-height: revert; + min-width: revert; + min-height: revert; + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + /* border-color: theme("borderColor.DEFAULT", currentColor); */ + } + + ::before, + ::after { + --tw-content: ""; + } + + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + */ + + html { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + tab-size: 4; /* 3 */ + font-family: Inter, -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif; /* 4 */ + } + + /* + 1. Remove the margin in all browsers. + 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. + */ + + body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ + } + + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Ensure horizontal rules are visible by default. + */ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. + */ + + abbr:where([title]) { + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. + */ + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0; + padding: 0; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. + */ + + a { + color: inherit; + text-decoration: inherit; + } + + /* + Add the correct font weight in Edge and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured `mono` font family by default. + 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp, + pre { + font-family: Consolas, Liberation Mono, Menlo, Courier, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /* + Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. + */ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + margin: 0; + width: auto; + max-width: auto; + } + + table td, + table th { + padding: unset; + vertical-align: middle; + text-align: left; + line-height: auto; + -webkit-user-select: text; + -moz-user-select: text; + user-select: text; + } + + /* + 1. Change the font styles in all browsers. + 2. Remove the margin in Firefox and Safari. + 3. Remove default padding in all browsers. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + font-weight: inherit; /* 1 */ + line-height: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ + outline: none; + + /* Reset `border: none;` from global.css */ + border-width: 0; + border-style: solid; + } + + /* + Remove the inheritance of text transform in Edge and Firefox. + */ + + button, + select { + text-transform: none; + letter-spacing: inherit; + border-radius: inherit; + appearance: auto; + -webkit-appearance: auto; + background: unset; + } + + /* + 1. Correct the inability to style clickable types in iOS and Safari. + 2. Remove default button styles. + Note: Attribute selectors are wrapped rapped in :where() to give zero + specificity so any class can override. + */ + + button, + :where( + /* [type='button'], */ + [type='reset'], + [type='submit']) { + -webkit-appearance: button; /* 1 */ + background-color: transparent; /* 2 */ + background-image: none; /* 2 */ + } + + /* + Use the modern Firefox focus style for all focusable elements. + */ + + :-moz-focusring { + outline: none; + } + + /* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) + */ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. + */ + + progress { + vertical-align: baseline; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. + */ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + 1. Correct the odd appearance in Chrome and Safari. + 2. Correct the outline style in Safari. + */ + + [type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /* + Remove the inner padding in Chrome and Safari on macOS. + */ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Correct the inability to style clickable types in iOS and Safari. + 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* + Add the correct display in Chrome and Safari. + */ + + summary { + display: list-item; + } + + /* + Removes the default spacing and border for appropriate elements. + */ + + blockquote, + dl, + dd, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + figure, + p, + pre { + margin: 0; + } + + fieldset { + margin: 0; + padding: 0; + } + + legend { + padding: 0; + } + + ol, + ul, + menu { + list-style: none; + margin: 0; + padding: 0; + } + + li { + margin: unset; + line-height: unset; + } + + /* + Prevent resizing textareas horizontally by default. + */ + + textarea { + resize: vertical; + } + + /* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to the user's configured gray 400 color. + */ + + input::placeholder, + textarea::placeholder { + opacity: 1; /* 1 */ + @apply text-gray-500; /* 2 */ + } + + button:focus-visible, + input:focus-visible { + outline: none; + } + + /* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. + */ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) + */ + + img, + video { + max-width: 100%; + height: auto; + } +} diff --git a/apps/shade/src/app.ts b/apps/shade/src/app.ts new file mode 100644 index 0000000..8187208 --- /dev/null +++ b/apps/shade/src/app.ts @@ -0,0 +1,21 @@ +// App shell/provider/context and transitional domain utilities +export {default as ShadeApp} from '@/shade-app'; +export type {ShadeAppProps} from '@/shade-app'; +export {useFocusContext} from '@/providers/shade-provider'; + +export { + formatUrl, + isValidDomain, + formatQueryDate, + getRangeDates, + getRangeForStartDate, + formatDisplayDateWithRange, + centsToDollars, + sanitizeChartData, + getYRange, + getYRangeWithMinPadding, + getYRangeWithLargePadding, + calculateYAxisWidth, + formatMemberName, + getMemberInitials +} from './lib/app-utils'; diff --git a/apps/shade/src/components.ts b/apps/shade/src/components.ts new file mode 100644 index 0000000..3f73e4e --- /dev/null +++ b/apps/shade/src/components.ts @@ -0,0 +1,59 @@ +// UI components — basic reusable controls +export * from './components/ui/alert-dialog'; +export * from './components/ui/animated-number'; +export * from './components/ui/avatar'; +export * from './components/ui/badge'; +export * from './components/ui/banner'; +export * from './components/ui/breadcrumb'; +export * from './components/ui/button'; +export * from './components/ui/card'; +export * from './components/ui/chart'; +export * from './components/ui/checkbox'; +export * from './components/ui/command'; +export * from './components/ui/data-list'; +export * from './components/ui/dialog'; +export * from './components/ui/dropdown-menu'; +export * from './components/ui/dropzone'; +export * from './components/ui/empty-indicator'; +export * from './components/ui/field'; +export * from './components/ui/flag'; +export * from './components/ui/form'; +export * from './components/ui/gh-chart'; +export * from './components/ui/hover-card'; +export * from './components/ui/indicator'; +export * from './components/ui/input'; +export * from './components/ui/input-group'; +export * from './components/ui/kbd'; +export * from './components/ui/label'; +export * from './components/ui/loading-indicator'; +export * from './components/ui/multi-select-combobox'; +export * from './components/ui/navbar'; +export * from './components/ui/no-value-label'; +export * from './components/ui/pagemenu'; +export * from './components/ui/popover'; +export * from './components/ui/right-sidebar'; +export * from './components/ui/separator'; +export * from './components/ui/select'; +export * from './components/ui/simple-pagination'; +export * from './components/ui/sheet'; +export * from './components/ui/sidebar'; +export * from './components/ui/skeleton'; +export * from './components/ui/sonner'; +export * from './components/ui/switch'; +export * from './components/ui/table'; +export * from './components/ui/tabs'; +export * from './components/ui/textarea'; +export * from './components/ui/toggle-group'; +export * from './components/ui/tooltip'; + +export type {DropdownMenuCheckboxItemProps as DropdownMenuCheckboxItemProps} from '@radix-ui/react-dropdown-menu'; + +export {IconComponents as Icon} from './components/ui/icon'; + +// Visual assets +export {ReactComponent as FacebookLogo} from './assets/images/facebook-logo.svg'; +export {ReactComponent as GhostLogo} from './assets/images/ghost-logo.svg'; +export {ReactComponent as GhostOrb} from './assets/images/ghost-orb.svg'; +export {ReactComponent as GoogleLogo} from './assets/images/google-logo.svg'; +export {ReactComponent as TwitterLogo} from './assets/images/twitter-logo.svg'; +export {ReactComponent as XLogo} from './assets/images/x-logo.svg'; diff --git a/apps/shade/src/index.ts b/apps/shade/src/index.ts new file mode 100644 index 0000000..489a216 --- /dev/null +++ b/apps/shade/src/index.ts @@ -0,0 +1,6 @@ +// Root compatibility lane for DS layers only. +// New code should import from layer-specific subpaths. +export * from './tokens'; +export * from './primitives'; +export * from './components'; +export * from './patterns'; diff --git a/apps/shade/src/patterns.ts b/apps/shade/src/patterns.ts new file mode 100644 index 0000000..9781c11 --- /dev/null +++ b/apps/shade/src/patterns.ts @@ -0,0 +1,8 @@ +// Feature-level compositions and pattern contracts +export * from './components/ui/filters'; +export {default as ColorPicker} from './components/features/color-picker/color-picker'; +export type {ColorPickerProps} from './components/features/color-picker/color-picker'; +export {default as PostShareModal} from './components/features/post-share-modal'; +export * from './components/features/table-filter-tabs/table-filter-tabs'; +export * from './components/features/utm-campaign-tabs/utm-campaign-tabs'; +export type {CampaignType, TabType} from './components/features/utm-campaign-tabs/utm-campaign-tabs'; diff --git a/apps/shade/src/primitives.ts b/apps/shade/src/primitives.ts new file mode 100644 index 0000000..42d2f07 --- /dev/null +++ b/apps/shade/src/primitives.ts @@ -0,0 +1,11 @@ +// Composition primitives +export * from './components/primitives'; + +// Legacy layout/structure compatibility exports +// Deprecated in MS-3: keep for compatibility during primitive-layer migration. +export * from './components/layout/page'; +export {ErrorPage} from './components/layout/error-page'; +export * from './components/layout/heading'; +export * from './components/layout/header'; +export * from './components/layout/list-header'; +export * from './components/layout/view-header'; diff --git a/apps/shade/src/shade-app.tsx b/apps/shade/src/shade-app.tsx new file mode 100644 index 0000000..ee29195 --- /dev/null +++ b/apps/shade/src/shade-app.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx'; +import React from 'react'; +// import {FetchKoenigLexical} from './global/form/HtmlEditor'; +import ShadeProvider from './providers/shade-provider'; + +/** + * The className is used to scope the styles of the app to the app's namespace. + * Some components in radixUI/ShadCN need to be wrapped in a div with the className + * in order to work correctly. + */ +export const SHADE_APP_NAMESPACES = 'shade shade-admin shade-activitypub shade-stats shade-posts'; + +export interface ShadeAppProps extends React.HTMLProps { + darkMode: boolean; + fetchKoenigLexical: null; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ShadeApp: React.FC = ({darkMode, fetchKoenigLexical, className, children, ...props}) => { + const appClassName = clsx( + 'shade', + className + ); + + return ( +
+ + {children} + +
+ ); +}; + +export default ShadeApp; diff --git a/apps/shade/src/tokens.ts b/apps/shade/src/tokens.ts new file mode 100644 index 0000000..1bb4f35 --- /dev/null +++ b/apps/shade/src/tokens.ts @@ -0,0 +1,357 @@ +// Token helpers generated from Tailwind + runtime CSS variable surfaces. +export const SHADE_TOKEN_NAMES = [ + 'accent', + 'accent-foreground', + 'animate-accordion-down', + 'animate-accordion-up', + 'animate-fade-in', + 'animate-fade-out', + 'animate-modal-backdrop-in', + 'animate-modal-in', + 'animate-modal-in-from-right', + 'animate-modal-in-reverse', + 'animate-setting-highlight-fade-out', + 'animate-spin', + 'animate-toaster-in', + 'animate-toaster-out', + 'animate-toaster-top-in', + 'background', + 'border', + 'breakpoint-lg', + 'breakpoint-md', + 'breakpoint-sidebar', + 'breakpoint-sidebarlg', + 'breakpoint-sm', + 'breakpoint-tablet', + 'breakpoint-xl', + 'breakpoint-xxl', + 'breakpoint-xxxl', + 'card', + 'card-foreground', + 'chart-1', + 'chart-2', + 'chart-3', + 'chart-4', + 'chart-5', + 'chart-amber', + 'chart-blue', + 'chart-darkblue', + 'chart-darkgray', + 'chart-gray', + 'chart-green', + 'chart-orange', + 'chart-purple', + 'chart-rose', + 'chart-teal', + 'chart-yellow', + 'color-accent', + 'color-accent-foreground', + 'color-background', + 'color-black', + 'color-blue', + 'color-blue-100', + 'color-blue-400', + 'color-blue-500', + 'color-blue-600', + 'color-blue-700', + 'color-border', + 'color-border-default', + 'color-border-strong', + 'color-border-subtle', + 'color-card', + 'color-card-foreground', + 'color-chart-1', + 'color-chart-2', + 'color-chart-3', + 'color-chart-4', + 'color-chart-5', + 'color-chart-amber', + 'color-chart-blue', + 'color-chart-darkgray', + 'color-chart-gray', + 'color-chart-green', + 'color-chart-orange', + 'color-chart-purple', + 'color-chart-rose', + 'color-chart-teal', + 'color-chart-yellow', + 'color-current', + 'color-destructive', + 'color-destructive-foreground', + 'color-focus-ring', + 'color-foreground', + 'color-ghostaccent', + 'color-gray', + 'color-gray-100', + 'color-gray-150', + 'color-gray-200', + 'color-gray-250', + 'color-gray-300', + 'color-gray-400', + 'color-gray-50', + 'color-gray-500', + 'color-gray-600', + 'color-gray-700', + 'color-gray-75', + 'color-gray-800', + 'color-gray-900', + 'color-gray-925', + 'color-gray-950', + 'color-gray-975', + 'color-green', + 'color-green-100', + 'color-green-400', + 'color-green-500', + 'color-green-600', + 'color-grey', + 'color-grey-100', + 'color-grey-150', + 'color-grey-200', + 'color-grey-250', + 'color-grey-300', + 'color-grey-400', + 'color-grey-50', + 'color-grey-500', + 'color-grey-600', + 'color-grey-700', + 'color-grey-75', + 'color-grey-800', + 'color-grey-900', + 'color-grey-925', + 'color-grey-950', + 'color-grey-975', + 'color-input', + 'color-lime', + 'color-muted', + 'color-muted-foreground', + 'color-orange', + 'color-orange-100', + 'color-orange-400', + 'color-orange-500', + 'color-orange-600', + 'color-pink', + 'color-pink-100', + 'color-pink-400', + 'color-pink-500', + 'color-pink-600', + 'color-popover', + 'color-popover-foreground', + 'color-primary', + 'color-primary-foreground', + 'color-purple', + 'color-purple-100', + 'color-purple-400', + 'color-purple-500', + 'color-purple-600', + 'color-red', + 'color-red-100', + 'color-red-400', + 'color-red-500', + 'color-red-600', + 'color-ring', + 'color-secondary', + 'color-secondary-foreground', + 'color-sidebar', + 'color-sidebar-accent', + 'color-sidebar-accent-foreground', + 'color-sidebar-border', + 'color-sidebar-foreground', + 'color-sidebar-primary', + 'color-sidebar-primary-foreground', + 'color-sidebar-ring', + 'color-state-danger', + 'color-state-danger-foreground', + 'color-state-info', + 'color-state-info-foreground', + 'color-state-success', + 'color-state-success-foreground', + 'color-state-warning', + 'color-state-warning-foreground', + 'color-surface-elevated', + 'color-surface-elevated-foreground', + 'color-surface-inverse', + 'color-surface-inverse-foreground', + 'color-surface-overlay', + 'color-surface-overlay-foreground', + 'color-surface-page', + 'color-surface-panel', + 'color-surface-panel-foreground', + 'color-text-inverse', + 'color-text-primary', + 'color-text-secondary', + 'color-text-tertiary', + 'color-transparent', + 'color-white', + 'color-yellow', + 'color-yellow-100', + 'color-yellow-400', + 'color-yellow-500', + 'color-yellow-600', + 'container-0', + 'container-2xl', + 'container-3xl', + 'container-4xl', + 'container-5xl', + 'container-6xl', + 'container-7xl', + 'container-8xl', + 'container-9xl', + 'container-lg', + 'container-md', + 'container-page', + 'container-pageminsidebar', + 'container-prose', + 'container-sm', + 'container-xl', + 'container-xs', + 'destructive', + 'destructive-foreground', + 'duration-base', + 'duration-fast', + 'duration-slow', + 'ease-emphasized', + 'ease-standard', + 'font-body', + 'font-cardo', + 'font-chakra-petch', + 'font-code', + 'font-fira-mono', + 'font-fira-sans', + 'font-heading', + 'font-ibm-plex-serif', + 'font-inherit', + 'font-inter', + 'font-jetbrains-mono', + 'font-lora', + 'font-manrope', + 'font-merriweather', + 'font-mono', + 'font-noto-sans', + 'font-noto-serif', + 'font-nunito', + 'font-old-standard-tt', + 'font-poppins', + 'font-prata', + 'font-roboto', + 'font-rufina', + 'font-sans', + 'font-serif', + 'font-space-grotesk', + 'font-space-mono', + 'font-tenor-sans', + 'foreground', + 'input', + 'input-group-radius', + 'leading-base', + 'leading-body', + 'leading-heading', + 'leading-loose', + 'leading-none', + 'leading-normal', + 'leading-relaxed', + 'leading-snug', + 'leading-supertight', + 'leading-tight', + 'leading-tighter', + 'mobile-navbar-height', + 'muted', + 'muted-foreground', + 'popover', + 'popover-foreground', + 'primary', + 'primary-foreground', + 'radius', + 'radius-2xl', + 'radius-3xl', + 'radius-badge', + 'radius-control', + 'radius-full', + 'radius-lg', + 'radius-md', + 'radius-pill', + 'radius-sm', + 'radius-surface', + 'radius-xl', + 'radius-xs', + 'ring', + 'secondary', + 'secondary-foreground', + 'border-default', + 'border-strong', + 'border-subtle', + 'control-height', + 'focus-ring', + 'state-danger', + 'state-danger-foreground', + 'state-info', + 'state-info-foreground', + 'state-success', + 'state-success-foreground', + 'state-warning', + 'state-warning-foreground', + 'surface-elevated', + 'surface-elevated-foreground', + 'surface-inverse', + 'surface-inverse-foreground', + 'surface-overlay', + 'surface-overlay-foreground', + 'surface-page', + 'surface-panel', + 'surface-panel-foreground', + 'text-inverse', + 'text-primary', + 'text-secondary', + 'text-tertiary', + 'shadow', + 'shadow-inner', + 'shadow-lg', + 'shadow-md', + 'shadow-md-heavy', + 'shadow-none', + 'shadow-sm', + 'shadow-xl', + 'shadow-xs', + 'sidebar-accent', + 'sidebar-accent-foreground', + 'sidebar-background', + 'sidebar-border', + 'sidebar-foreground', + 'sidebar-primary', + 'sidebar-primary-foreground', + 'sidebar-ring', + 'spacing', + 'text-2xl', + 'text-2xs', + 'text-3xl', + 'text-4xl', + 'text-5xl', + 'text-5xl--line-height', + 'text-6xl', + 'text-6xl--line-height', + 'text-7xl', + 'text-7xl--line-height', + 'text-8xl', + 'text-8xl--line-height', + 'text-9xl', + 'text-9xl--line-height', + 'text-base', + 'text-body-lg', + 'text-body-md', + 'text-body-sm', + 'text-lg', + 'text-md', + 'text-sm', + 'text-xl', + 'text-xs', + 'tracking-normal', + 'tracking-tight', + 'tracking-tighter', + 'tracking-tightest', + 'tracking-wide', + 'tracking-wider', + 'tracking-widest' +] as const; + +export type ShadeTokenName = (typeof SHADE_TOKEN_NAMES)[number]; + +export const shadeToken = (name: ShadeTokenName): string => `var(--${name})`; diff --git a/apps/shade/src/typings.d.ts b/apps/shade/src/typings.d.ts new file mode 100644 index 0000000..d733c0d --- /dev/null +++ b/apps/shade/src/typings.d.ts @@ -0,0 +1,7 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; +} diff --git a/apps/shade/src/utils.ts b/apps/shade/src/utils.ts new file mode 100644 index 0000000..711e8d9 --- /dev/null +++ b/apps/shade/src/utils.ts @@ -0,0 +1,21 @@ +// DS-safe utilities, generic hooks, and third-party namespaces +export * as Recharts from 'recharts'; +export * as LucideIcon from 'lucide-react'; + +export {default as useGlobalDirtyState} from './hooks/use-global-dirty-state'; +export {useSimplePagination} from './hooks/use-simple-pagination'; + +export { + cn, + debounce, + kebabToPascalCase, + formatTimestamp, + formatNumber, + formatDuration, + formatPercentage, + formatDisplayDate, + formatDisplayTime, + getCountryFlag, + stringToHslColor, + abbreviateNumber +} from './lib/ds-utils'; diff --git a/apps/shade/styles.css b/apps/shade/styles.css new file mode 100644 index 0000000..2d1ea04 --- /dev/null +++ b/apps/shade/styles.css @@ -0,0 +1,102 @@ +@import url(https://fonts.bunny.net/css?family=cardo:400,700); +@import url(https://fonts.bunny.net/css?family=manrope:300,500,700); +@import url(https://fonts.bunny.net/css?family=merriweather:300,700); +@import url(https://fonts.bunny.net/css?family=nunito:400,600,700); +@import url(https://fonts.bunny.net/css?family=old-standard-tt:400,700); +@import url(https://fonts.bunny.net/css?family=prata:400); +@import url(https://fonts.bunny.net/css?family=roboto:400,500,700); +@import url(https://fonts.bunny.net/css?family=rufina:400,500,700); +@import url(https://fonts.bunny.net/css?family=tenor-sans:400); +@import url(https://fonts.bunny.net/css?family=space-grotesk:700); +@import url(https://fonts.bunny.net/css?family=chakra-petch:400); +@import url(https://fonts.bunny.net/css?family=noto-sans:400,700); +@import url(https://fonts.bunny.net/css?family=poppins:400,700); +@import url(https://fonts.bunny.net/css?family=fira-sans:400,700); +@import url(https://fonts.bunny.net/css?family=inter:400,700); +@import url(https://fonts.bunny.net/css?family=noto-serif:400,700); +@import url(https://fonts.bunny.net/css?family=lora:400,700); +@import url(https://fonts.bunny.net/css?family=ibm-plex-serif:400,700); +@import url(https://fonts.bunny.net/css?family=space-mono:400,700); +@import url(https://fonts.bunny.net/css?family=fira-mono:400,700); +@import url(https://fonts.bunny.net/css?family=jetbrains-mono:400,700); + +@import "tailwindcss/theme.css"; +@import "./preflight.css"; +@import "tailwindcss/utilities.css"; +@import "tw-animate-css"; +@import "./tailwind.theme.css"; + +@import "./theme-variables.css"; + +@layer base { + /* This just serves as a placeholder; we actually load Inter from a font file in Ember admin */ + @font-face { + font-family: "Inter"; + src: local("Inter") format("truetype-variations"); + font-weight: 100 900; + } + + .shade { + & { + @apply font-sans text-black text-base leading-normal; + } + } + + * { + @apply border-border; + } + body { + @apply font-sans antialiased bg-background text-foreground; + } +} + +.shade { + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + letter-spacing: unset; +} + +.shade.app-container { + height: 100vh; + width: 100%; + overflow-x: hidden; + overflow-y: auto; +} + +@media (max-width: 800px) { + body:not(.react-admin) .shade { + height: calc(100vh - var(--mobile-navbar-height)); + } +} + +.dark .shade { + color: #fafafb; +} + +.dark .shade .gh-loading-orb-container { + background-color: #000000; +} + +.dark .shade .gh-loading-orb { + filter: invert(100%); +} + +.shade .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome */ +} + +.shade .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +/* Prose classes are for formatting arbitrary HTML that comes from the API */ +.gh-prose-links a { + color: #30cf43; +} diff --git a/apps/shade/tailwind.theme.css b/apps/shade/tailwind.theme.css new file mode 100644 index 0000000..f4f51a2 --- /dev/null +++ b/apps/shade/tailwind.theme.css @@ -0,0 +1,401 @@ +/* + * CSS-first token source for Tailwind v4 runtime generation. + */ + +@custom-variant dark (&:is(.dark *):not(.light *)); + +@theme { + --breakpoint-sm: 480px; + --breakpoint-md: 640px; + --breakpoint-sidebar: 800px; + --breakpoint-lg: 1024px; + --breakpoint-sidebarlg: 1240px; + --breakpoint-xl: 1320px; + --breakpoint-xxl: 1440px; + --breakpoint-xxxl: 1600px; + --breakpoint-tablet: 860px; + --color-transparent: transparent; + --color-current: currentColor; + --color-ghostaccent: var(--accent-color, #ff0095); + --color-white: #FFF; + --color-black: #15171A; + --color-grey-50: #FAFAFB; + --color-grey-75: #F9FAFB; + --color-grey-100: #F4F5F6; + --color-grey-150: #F1F3F4; + --color-grey-200: #EBEEF0; + --color-grey-250: #E5E9ED; + --color-grey-300: #DDE1E5; + --color-grey-400: #CED4D9; + --color-grey-500: #AEB7C1; + --color-grey-600: #95A1AD; + --color-grey-700: #7C8B9A; + --color-grey-800: #626D79; + --color-grey-900: #394047; + --color-grey-925: #2E3338; + --color-grey-950: #222427; + --color-grey-975: #191B1E; + --color-grey: #ABB4BE; + --color-gray-50: #FAFAFB; + --color-gray-75: #F9FAFB; + --color-gray-100: #F4F5F6; + --color-gray-150: #F1F3F4; + --color-gray-200: #EBEEF0; + --color-gray-250: #E5E9ED; + --color-gray-300: #DDE1E5; + --color-gray-400: #CED4D9; + --color-gray-500: #AEB7C1; + --color-gray-600: #95A1AD; + --color-gray-700: #7C8B9A; + --color-gray-800: #626D79; + --color-gray-900: #394047; + --color-gray-925: #2E3338; + --color-gray-950: #222427; + --color-gray-975: #191B1E; + --color-gray: #ABB4BE; + --color-green-100: #E1F9E4; + --color-green-400: #58DA67; + --color-green-500: #30CF43; + --color-green-600: #2AB23A; + --color-green: #30CF43; + --color-blue-100: #DBF4FF; + --color-blue-400: #42C6FF; + --color-blue-500: #14B8FF; + --color-blue-600: #00A4EB; + --color-blue-700: #3778F1; + --color-blue: #14B8FF; + --color-purple-100: #EDE0FF; + --color-purple-400: #A366FF; + --color-purple-500: #8E42FF; + --color-purple-600: #7B1FFF; + --color-purple: #8E42FF; + --color-pink-100: #FFDFEE; + --color-pink-400: #FF5CA8; + --color-pink-500: #FB2D8D; + --color-pink-600: #F70878; + --color-pink: #FB2D8D; + --color-red-100: #FFE0E0; + --color-red-400: #F9394C; + --color-red-500: #F50B23; + --color-red-600: #DC091E; + --color-red: #F50B23; + --color-orange-100: #FFEDD5; + --color-orange-400: #FB923C; + --color-orange-500: #F97316; + --color-orange-600: #EA580C; + --color-orange: #F97316; + --color-yellow-100: #FFF1D6; + --color-yellow-400: #FFC247; + --color-yellow-500: #FFB41F; + --color-yellow-600: #F0A000; + --color-yellow: #FFB41F; + --color-lime: #B5FF18; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-surface-page: var(--surface-page); + --color-surface-panel: var(--surface-panel); + --color-surface-panel-foreground: var(--surface-panel-foreground); + --color-surface-elevated: var(--surface-elevated); + --color-surface-elevated-foreground: var(--surface-elevated-foreground); + --color-surface-overlay: var(--surface-overlay); + --color-surface-overlay-foreground: var(--surface-overlay-foreground); + --color-surface-inverse: var(--surface-inverse); + --color-surface-inverse-foreground: var(--surface-inverse-foreground); + --color-text-primary: var(--text-primary); + --color-text-secondary: var(--text-secondary); + --color-text-tertiary: var(--text-tertiary); + --color-text-inverse: var(--text-inverse); + --color-border-subtle: var(--border-subtle); + --color-border-default: var(--border-default); + --color-border-strong: var(--border-strong); + --color-focus-ring: var(--focus-ring); + --color-state-info: var(--state-info); + --color-state-info-foreground: var(--state-info-foreground); + --color-state-success: var(--state-success); + --color-state-success-foreground: var(--state-success-foreground); + --color-state-warning: var(--state-warning); + --color-state-warning-foreground: var(--state-warning-foreground); + --color-state-danger: var(--state-danger); + --color-state-danger-foreground: var(--state-danger-foreground); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-chart-gray: var(--chart-gray); + --color-chart-darkgray: var(--chart-darkgray); + --color-chart-rose: var(--chart-rose); + --color-chart-orange: var(--chart-orange); + --color-chart-amber: var(--chart-amber); + --color-chart-yellow: var(--chart-yellow); + --color-chart-green: var(--chart-green); + --color-chart-teal: var(--chart-teal); + --color-chart-blue: var(--chart-blue); + --color-chart-purple: var(--chart-purple); + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --font-cardo: Cardo; + --font-manrope: Manrope; + --font-merriweather: Merriweather; + --font-nunito: Nunito; + --font-tenor-sans: Tenor Sans; + --font-old-standard-tt: Old Standard TT; + --font-prata: Prata; + --font-roboto: Roboto; + --font-rufina: Rufina; + --font-inter: Inter; + --font-body: var(--font-sans); + --font-heading: var(--font-sans); + --font-code: var(--font-mono); + --font-sans: Inter, -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif; + --font-serif: Georgia, serif; + --font-mono: Consolas, Liberation Mono, Menlo, Courier, monospace; + --font-inherit: inherit; + --font-space-grotesk: Space Grotesk; + --font-chakra-petch: Chakra Petch; + --font-noto-sans: Noto Sans; + --font-poppins: Poppins; + --font-fira-sans: Fira Sans; + --font-noto-serif: Noto Serif; + --font-lora: Lora; + --font-ibm-plex-serif: IBM Plex Serif; + --font-space-mono: Space Mono; + --font-fira-mono: Fira Mono; + --font-jetbrains-mono: JetBrains Mono; + --tracking-tightest: -.05em; + --tracking-tighter: -.025em; + --tracking-tight: -.01em; + --tracking-normal: 0; + --tracking-wide: .01em; + --tracking-wider: .025em; + --tracking-widest: .5em; + --shadow: 0 0 1px rgba(0,0,0,.05), 0 5px 18px rgba(0,0,0,.08); + --shadow-xs: 0 0 1px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.03), 0 8px 10px -12px rgba(0,0,0,.1); + --shadow-sm: 0 0 1px rgba(0,0,0,.12), 0 1px 6px rgba(0,0,0,0.03), 0 8px 10px -8px rgba(0,0,0,.1); + --shadow-md: 0 0 1px rgba(0,0,0,0.12), 0 1px 6px rgba(0,0,0,0.03), 0 8px 10px -8px rgba(0,0,0,0.05), 0px 24px 37px -21px rgba(0, 0, 0, 0.05); + --shadow-md-heavy: 0 0 1px rgba(0,0,0,0.22), 0 1px 6px rgba(0,0,0,0.15), 0 8px 10px -8px rgba(0,0,0,0.16), 0px 24px 37px -21px rgba(0, 0, 0, 0.46); + --shadow-lg: 0 0 7px rgba(0, 0, 0, 0.08), 0 2.1px 2.2px -5px rgba(0, 0, 0, 0.011), 0 5.1px 5.3px -5px rgba(0, 0, 0, 0.016), 0 9.5px 10px -5px rgba(0, 0, 0, 0.02), 0 17px 17.9px -5px rgba(0, 0, 0, 0.024), 0 31.8px 33.4px -5px rgba(0, 0, 0, 0.029), 0 76px 80px -5px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 2.8px 2.2px rgba(0, 0, 0, 0.02), 0 6.7px 5.3px rgba(0, 0, 0, 0.028), 0 12.5px 10px rgba(0, 0, 0, 0.035), 0 22.3px 17.9px rgba(0, 0, 0, 0.042), 0 41.8px 33.4px rgba(0, 0, 0, 0.05), 0 100px 80px rgba(0, 0, 0, 0.07); + --shadow-inner: inset 0 0 4px 0 rgb(0 0 0 / 0.08); + --shadow-none: 0 0 #0000; + --spacing: 0.4rem; + --container-0: 0rem; + --container-xs: 32rem; + --container-sm: 38.4rem; + --container-md: 44.8rem; + --container-lg: 51.2rem; + --container-xl: 57.6rem; + --container-2xl: 67.2rem; + --container-3xl: 76.8rem; + --container-4xl: 89.6rem; + --container-5xl: 102.4rem; + --container-6xl: 115.2rem; + --container-7xl: 132rem; + --container-8xl: 140rem; + --container-9xl: 156rem; + --container-prose: 65ch; + --container-page: 148rem; + --container-pageminsidebar: 116rem; + /* Preserve legacy Tailwind v3 border radius scale for v4 rounded* utilities. */ + --radius: 0.4rem; + --radius-xs: 0.3rem; + --radius-sm: 0.4rem; + --radius-md: 0.6rem; + --radius-lg: 0.8rem; + --radius-xl: 1.2rem; + --radius-2xl: 1.6rem; + --radius-3xl: 2.4rem; + --radius-full: 9999px; + --radius-control: var(--radius-md); + --radius-surface: var(--radius-lg); + --radius-badge: var(--radius-full); + --radius-pill: var(--radius-full); + --text-2xs: 1.0rem; + --text-base: 1.4rem; + --text-xs: 1.2rem; + --text-sm: 1.3rem; + --text-md: 1.4rem; + --text-lg: 1.5rem; + --text-xl: 1.7rem; + --text-2xl: 2.2rem; + --text-3xl: 2.8rem; + --text-4xl: 3.2rem; + --text-5xl: 4.0rem; + --text-5xl--line-height: 1.15; + --text-6xl: 5.8rem; + --text-6xl--line-height: 1; + --text-7xl: 7.0rem; + --text-7xl--line-height: 1; + --text-8xl: 9.6rem; + --text-8xl--line-height: 1; + --text-9xl: 12.8rem; + --text-9xl--line-height: 1; + --text-body-sm: var(--text-sm); + --text-body-md: var(--text-base); + --text-body-lg: var(--text-lg); + --leading-none: 1; + --leading-snug: 1.375; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --leading-loose: 2; + --leading-base: 1.5em; + --leading-tight: 1.35em; + --leading-tighter: 1.25em; + --leading-supertight: 1.1em; + --leading-body: var(--leading-base); + --leading-heading: var(--leading-tight); + --duration-fast: 150ms; + --duration-base: 250ms; + --duration-slow: 400ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1); + --animate-toaster-in: toasterIn 0.8s cubic-bezier(0.445, 0.050, 0.550, 0.950); + --animate-toaster-out: toasterOut 0.4s 0s 1 ease forwards; + --animate-toaster-top-in: toasterTopIn 0.8s cubic-bezier(0.445, 0.050, 0.550, 0.950); + --animate-fade-in: fadeIn 0.15s ease forwards; + --animate-fade-out: fadeOut 0.15s ease forwards; + --animate-setting-highlight-fade-out: fadeOut 0.2s 1.4s ease forwards; + --animate-modal-backdrop-in: fadeIn 0.15s ease forwards; + --animate-modal-in: modalIn 0.25s ease forwards; + --animate-modal-in-from-right: modalInFromRight 0.25s ease forwards; + --animate-modal-in-reverse: modalInReverse 0.25s ease forwards; + --animate-spin: spin 1s linear infinite; + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; +} + +@keyframes toasterIn { + 0.00% { + transform: translateY(100%); + } + 26.52% { + transform: translateY(-3.90px); + } + 63.26% { + transform: translateY(1.2px); + } + 100.00% { + transform: translateY(0px); + } +} + +@keyframes toasterTopIn { + 0.00% { + transform: translateY(-82px); + } + 26.52% { + transform: translateY(5.90px); + } + 63.26% { + transform: translateY(-1.77px); + } + 100.00% { + transform: translateY(0px); + } +} + +@keyframes toasterOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes modalIn { + 0% { + transform: translateY(32px); + } + 100% { + transform: translateY(0px); + } +} + +@keyframes modalInFromRight { + 0% { + transform: translateX(32px); + opacity: 0; + } + 100% { + transform: translateX(0px); + opacity: 1; + } +} + +@keyframes modalInReverse { + 0% { + transform: translateY(-32px); + } + 100% { + transform: translateY(0px); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } +} + +@keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} diff --git a/apps/shade/test/.eslintrc.cjs b/apps/shade/test/.eslintrc.cjs new file mode 100644 index 0000000..f95a58a --- /dev/null +++ b/apps/shade/test/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + env: { + browser: true + }, + extends: [ + 'plugin:ghost/test' + ], + rules: { + // Enforce a kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/apps/shade/theme-variables.css b/apps/shade/theme-variables.css new file mode 100644 index 0000000..275653a --- /dev/null +++ b/apps/shade/theme-variables.css @@ -0,0 +1,135 @@ +/* Runtime semantic variables shared by styles.css and tokens.css */ +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(216 11% 9%); + --muted: hsl(200 12% 96%); + --muted-foreground: hsl(210 13% 55%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(216 11% 9%); + --border: hsl(204 15% 91%); + --input: hsl(204 14% 93%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(216 11% 9%); + --primary: hsl(216 11% 9%); + --primary-foreground: hsl(0 0% 100%); + --secondary: hsl(204 14% 93%); + --secondary-foreground: hsl(216 11% 9%); + --accent: hsl(200 12% 96%); + --accent-foreground: hsl(216 11% 9%); + --destructive: hsl(354 92% 50%); + --destructive-foreground: hsl(0 0% 100%); + --ring: hsl(215 13% 63%); + --surface-page: var(--background); + --surface-panel: var(--card); + --surface-panel-foreground: var(--card-foreground); + --surface-elevated: hsl(0 0% 100%); + --surface-elevated-foreground: var(--card-foreground); + --surface-overlay: var(--popover); + --surface-overlay-foreground: var(--popover-foreground); + --surface-inverse: hsl(216 11% 9%); + --surface-inverse-foreground: hsl(0 0% 100%); + --text-primary: var(--foreground); + --text-secondary: hsl(210 13% 55%); + --text-tertiary: hsl(210 13% 63%); + --text-inverse: hsl(0 0% 100%); + --border-subtle: var(--input); + --border-default: var(--border); + --border-strong: hsl(210 13% 79%); + --focus-ring: var(--ring); + --state-info: hsl(198 100% 51%); + --state-info-foreground: hsl(216 11% 9%); + --state-success: hsl(144 100% 39%); + --state-success-foreground: hsl(216 11% 9%); + --state-warning: hsl(47 100% 50%); + --state-warning-foreground: hsl(216 11% 9%); + --state-danger: var(--destructive); + --state-danger-foreground: hsl(216 11% 5%); + --control-height: 34px; + --input-group-radius: 9px; + --chart-1: hsl(201 100% 50%); + --chart-2: hsl(201 88% 70%); + --chart-3: hsl(201 85% 80%); + --chart-4: hsl(201 82% 90%); + --chart-5: hsl(201 87% 94%); + --chart-gray: hsl(210 13% 88%); + --chart-darkgray: hsl(210 13% 79%); + --chart-rose: hsl(345 100% 56%); + --chart-orange: hsl(25 100% 50%); + --chart-amber: hsl(36 100% 50%); + --chart-yellow: hsl(47 100% 50%); + --chart-green: hsl(144 100% 39%); + --chart-teal: hsl(174 100% 37%); + --chart-blue: hsl(198 100% 51%); + --chart-darkblue: hsl(226 91% 61%); + --chart-purple: hsl(273 100% 64%); + --sidebar-background: hsl(240 11% 98%); + --sidebar-foreground: hsl(216 11% 9%); + --sidebar-primary: hsl(216 11% 9%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(204 14% 93%); + --sidebar-accent-foreground: hsl(216 11% 9%); + --sidebar-border: hsl(200 12% 96%); + --sidebar-ring: hsl(215 13% 63%); + --mobile-navbar-height: 64px; +} + +.dark { + --background: hsl(216 11% 9%); + --foreground: hsl(210 13% 88%); + --muted: hsl(210 11% 25%); + --muted-foreground: hsl(210 13% 63%); + --accent: hsl(210 11% 25%); + --accent-foreground: hsl(200 12% 96%); + --popover: hsl(216 11% 9%); + --popover-foreground: hsl(212 13% 72%); + --border: hsl(216 7% 14%); + --input: hsl(210 11% 25%); + --card: hsl(216 11% 9%); + --card-foreground: hsl(213 31% 91%); + --primary: hsl(200 12% 96%); + --primary-foreground: hsl(216 11% 9%); + --secondary: hsl(210 11% 25%); + --secondary-foreground: hsl(200 12% 96%); + --destructive: hsl(354 81% 31%); + --destructive-foreground: hsl(240 11% 98%); + --ring: hsl(210 11% 25%); + --surface-page: var(--background); + --surface-panel: var(--card); + --surface-panel-foreground: var(--card-foreground); + --surface-elevated: hsl(210 11% 12%); + --surface-elevated-foreground: hsl(213 31% 91%); + --surface-overlay: var(--popover); + --surface-overlay-foreground: var(--popover-foreground); + --surface-inverse: hsl(0 0% 100%); + --surface-inverse-foreground: hsl(216 11% 9%); + --text-primary: var(--foreground); + --text-secondary: hsl(210 13% 72%); + --text-tertiary: hsl(210 13% 63%); + --text-inverse: hsl(216 11% 9%); + --border-subtle: hsl(210 11% 20%); + --border-default: var(--border); + --border-strong: hsl(210 13% 55%); + --focus-ring: var(--ring); + --state-info: hsl(198 100% 64%); + --state-info-foreground: hsl(216 11% 9%); + --state-success: hsl(144 100% 48%); + --state-success-foreground: hsl(216 11% 9%); + --state-warning: hsl(47 100% 65%); + --state-warning-foreground: hsl(216 11% 9%); + --state-danger: hsl(354 81% 54%); + --state-danger-foreground: hsl(216 11% 5%); + --chart-1: hsl(201 100% 50%); + --chart-2: hsl(201 88% 70%); + --chart-3: hsl(201 85% 80%); + --chart-4: hsl(201 82% 90%); + --chart-5: hsl(201 87% 94%); + --chart-gray: hsl(210 13% 55%); + --sidebar-background: hsl(216 11% 6%); + --sidebar-foreground: hsl(200 12% 96%); + --sidebar-primary: hsl(210 11% 25%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(210 11% 17%); + --sidebar-accent-foreground: hsl(200 12% 96%); + --sidebar-border: hsl(210 11% 15%); + --sidebar-ring: hsl(210 13% 55%); +} diff --git a/apps/shade/tokens.css b/apps/shade/tokens.css new file mode 100644 index 0000000..7e59524 --- /dev/null +++ b/apps/shade/tokens.css @@ -0,0 +1,3 @@ +/* Token-only CSS entrypoint (no preflight or utility layers). */ +@import "./tailwind.theme.css"; +@import "./theme-variables.css"; diff --git a/apps/shade/tsconfig.declaration.json b/apps/shade/tsconfig.declaration.json new file mode 100644 index 0000000..d26eefa --- /dev/null +++ b/apps/shade/tsconfig.declaration.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} + \ No newline at end of file diff --git a/apps/shade/tsconfig.json b/apps/shade/tsconfig.json new file mode 100644 index 0000000..11e6a88 --- /dev/null +++ b/apps/shade/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "test"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/apps/shade/tsconfig.node.json b/apps/shade/tsconfig.node.json new file mode 100644 index 0000000..ede1f39 --- /dev/null +++ b/apps/shade/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "package.json"] +} diff --git a/apps/shade/vite.config.ts b/apps/shade/vite.config.ts new file mode 100644 index 0000000..b6b31ab --- /dev/null +++ b/apps/shade/vite.config.ts @@ -0,0 +1,88 @@ +import path from 'path'; +import react from '@vitejs/plugin-react'; +import {globSync} from 'glob'; +import {resolve} from 'path'; +import svgr from 'vite-plugin-svgr'; +import {defineConfig} from 'vitest/config'; + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + svgr(), + react() + ], + resolve: { + alias: { + '@': path.resolve(__dirname, './src') + } + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.VITEST_SEGFAULT_RETRY': 3 + }, + preview: { + port: 4174 + }, + build: { + reportCompressedSize: false, + minify: false, + sourcemap: true, + outDir: 'es', + lib: { + formats: ['es'], + entry: globSync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, libpath) => { + if (libpath.includes('.stories.') || libpath.endsWith('.d.ts')) { + return entries; + } + + const outPath = libpath.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, ''); + entries[outPath] = libpath; + return entries; + }, {} as Record) + }, + commonjsOptions: { + include: [/packages/, /node_modules/] + }, + rollupOptions: { + external: (source) => { + if (source.startsWith('@/')) { + return false; + } + + if (source.startsWith('.')) { + return false; + } + + if (source.includes('node_modules')) { + return true; + } + + return !source.includes(__dirname); + } + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + include: ['./test/unit/**/*'], + exclude: ['./test/unit/utils/test-utils.tsx'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.{js,jsx,ts,tsx}'], + exclude: [ + 'src/**/*.stories.{js,jsx,ts,tsx}', + 'src/**/*.d.ts', + 'src/types/**/*' + ] + }, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); +}); diff --git a/apps/signup-form/.env.development b/apps/signup-form/.env.development new file mode 100644 index 0000000..a5d21db --- /dev/null +++ b/apps/signup-form/.env.development @@ -0,0 +1,2 @@ +# Override this in .env.development.local if needed +VITE_SITE_URL=http://127.0.0.1:2368 diff --git a/apps/signup-form/.eslintrc.cjs b/apps/signup-form/.eslintrc.cjs new file mode 100644 index 0000000..984819b --- /dev/null +++ b/apps/signup-form/.eslintrc.cjs @@ -0,0 +1,51 @@ +/* eslint-env node */ +const tailwindConfig = `${__dirname}/tailwind.config.cjs`; + +module.exports = { + root: true, + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended' + ], + plugins: [ + 'ghost', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + } + }, + rules: { + // Sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // Suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // Ignore prop-types for now + 'react/prop-types': 'off', + + // Custom react rules + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + + 'tailwindcss/classnames-order': ['error', {config: tailwindConfig}], + 'tailwindcss/enforces-negative-arbitrary-values': ['warn', {config: tailwindConfig}], + 'tailwindcss/enforces-shorthand': ['warn', {config: tailwindConfig}], + 'tailwindcss/migration-from-tailwind-2': ['warn', {config: tailwindConfig}], + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': ['error', {config: tailwindConfig}] + } +}; diff --git a/apps/signup-form/.storybook/main.tsx b/apps/signup-form/.storybook/main.tsx new file mode 100644 index 0000000..23facc7 --- /dev/null +++ b/apps/signup-form/.storybook/main.tsx @@ -0,0 +1,27 @@ +import type { StorybookConfig } from "@storybook/react-vite"; +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + { + name: '@storybook/addon-styling', + }, + ], + framework: { + name: "@storybook/react-vite", + options: {}, + }, + docs: { + autodocs: "tag", + }, + // staticDirs: ['../public/fonts'], + async viteFinal(config, options) { + config.resolve.alias = { + crypto: require.resolve('rollup-plugin-node-builtins'), + } + return config; + }, +}; +export default config; diff --git a/apps/signup-form/.storybook/preview.tsx b/apps/signup-form/.storybook/preview.tsx new file mode 100644 index 0000000..9cf85af --- /dev/null +++ b/apps/signup-form/.storybook/preview.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import i18nLib from '@tryghost/i18n'; + +import type {Preview} from "@storybook/react"; +import './storybook.css'; +import {AppContextProvider, AppContextType} from '../src/app-context'; + +const transparencyGrid = `url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3ERectangle%3C/title%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23F2F6F8' d='M0 0h24v24H0z'/%3E%3Cpath fill='%23E5ECF0' d='M0 0h12v12H0zM12 12h12v12H12z'/%3E%3C/g%3E%3C/svg%3E")` + +const preview: Preview = { + parameters: { + actions: {argTypesRegex: "^on[A-Z].*"}, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + options: { + storySort: { + order: ['Global', 'Settings', 'Experimental'], + }, + }, + }, + globalTypes: { + locale: { + description: 'Internationalization locale', + defaultValue: 'en', + toolbar: { + icon: 'globe', + items: [ + {value: 'en', right: '🇺🇸', title: 'English'}, + {value: 'nl', right: '🇳🇱', title: 'Nederlands'}, + ], + }, + }, + }, + decorators: [ + (Story, context) => ( +
+ {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} + +
+ ), + + (Story, {context, globals}) => { + const i18n = i18nLib(globals.locale || 'en', 'signup-form'); + const c: AppContextType = { + page: { + name: 'FormPage', + data: {} + }, + setPage: () => { }, + api: { + sendMagicLink: async () => { + // Sleep to ensure the loading state is visible enough + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + + return true; + } + }, + t: i18n.t, + scriptTag: document.createElement('div'), + options: { + site: 'localhost', + locale: globals.locale || 'en', + title: 'Signup Forms Weekly', + description: 'An independent publication about embeddable signup forms.', + buttonColor: '#121212', + backgroundColor: '#ffffff', + labels: [], + ...context + } + }; + + return ( + {/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} + + ); + } + ], +}; +export default preview; diff --git a/apps/signup-form/.storybook/storybook.css b/apps/signup-form/.storybook/storybook.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/apps/signup-form/.storybook/storybook.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/signup-form/LICENSE b/apps/signup-form/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/apps/signup-form/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/signup-form/README.md b/apps/signup-form/README.md new file mode 100644 index 0000000..445bec8 --- /dev/null +++ b/apps/signup-form/README.md @@ -0,0 +1,69 @@ +# Embeddable Signup Form + +Embed a Ghost signup form on any site. + +## Development + +### Pre-requisites + +- Run `pnpm` in Ghost monorepo root +- Run `pnpm` in this directory + +### Running via Ghost `pnpm dev` in root folder + +Signup Form runs automatically when using Ghost's development command from the monorepo root: +```bash +pnpm dev +``` + +This starts all frontend apps (including Signup Form.) + +### Running the development version only + +Run `pnpm dev` (in package folder) to start the development server to test/develop the form standalone. +- This will generate a demo site on http://localhost:6173 +- This will build and watch the production build and host it on http://localhost:6174/signup-form.min.js (different port!) + +### Using the UMD build during development + +Vite by default only supports HRM with an ESM output. But when loading a script on a site as a ESM module (` +
+ +
+

Without icon

+ +
+ +
+ +
+

Minimal

+ +
+ +
+ +
+

Minimal centered

+ +
+ +
+ +
+

Minimal full width

+ +
+ +
+ +
+

With invalid configuration

+

When you submit this one, it will throw an error.

+ +
+ +
+ +
+

Translated

+

Use production build, since the multiple languages aren't working well with modules.

+ + + diff --git a/apps/signup-form/package.json b/apps/signup-form/package.json new file mode 100644 index 0000000..148dcda --- /dev/null +++ b/apps/signup-form/package.json @@ -0,0 +1,71 @@ +{ + "name": "@tryghost/signup-form", + "version": "0.3.14", + "license": "MIT", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "files": [ + "LICENSE", + "README.md", + "umd/" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "dev": "concurrently \"vite --port 6173\" \"vite preview -l silent\" \"vite build --watch\"", + "preview": "concurrently \"vite preview -l silent\" \"vite build --watch\"", + "dev:test": "vite build && vite preview --port 6175", + "build": "tsc && vite build", + "lint": "pnpm run lint:js", + "lint:js": "eslint --ext .js,.ts,.cjs,.tsx --cache src test", + "test:unit": "pnpm build", + "test:e2e": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", + "test:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:e2e --headed", + "test:e2e:full": "ALL_BROWSERS=1 pnpm test:e2e", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "preship": "pnpm lint", + "ship": "node ../../.github/scripts/release-apps.js", + "prepublishOnly": "pnpm build" + }, + "dependencies": { + "@tryghost/debug": "0.1.40", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@playwright/test": "1.59.1", + "@storybook/addon-essentials": "8.6.14", + "@storybook/addon-interactions": "8.6.14", + "@storybook/addon-links": "8.6.14", + "@storybook/addon-styling": "1.3.7", + "@storybook/blocks": "8.6.14", + "@storybook/react": "8.6.14", + "@storybook/react-vite": "8.6.14", + "@storybook/testing-library": "0.2.2", + "@tailwindcss/line-clamp": "0.4.4", + "@tryghost/i18n": "workspace:*", + "@types/react": "18.3.28", + "@types/react-dom": "18.3.7", + "@vitejs/plugin-react": "4.7.0", + "autoprefixer": "10.4.21", + "concurrently": "8.2.2", + "eslint": "catalog:", + "eslint-plugin-react-hooks": "4.6.2", + "eslint-plugin-react-refresh": "0.4.24", + "eslint-plugin-tailwindcss": "3.18.2", + "jsdom": "28.1.0", + "postcss": "8.5.6", + "postcss-import": "16.1.1", + "prop-types": "15.8.1", + "rollup-plugin-node-builtins": "2.1.2", + "storybook": "8.6.15", + "stylelint": "15.11.0", + "tailwindcss": "3.4.18", + "vite": "5.4.21", + "vite-plugin-svgr": "3.3.0", + "vitest": "1.6.1" + } +} diff --git a/apps/signup-form/playwright.config.ts b/apps/signup-form/playwright.config.ts new file mode 100644 index 0000000..c9772ea --- /dev/null +++ b/apps/signup-form/playwright.config.ts @@ -0,0 +1,58 @@ +import {defineConfig, devices} from '@playwright/test'; + +export const E2E_PORT = 6175; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './test/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Hardcode to use all cores in CI */ + workers: process.env.CI ? '100%' : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + launchOptions: { + slowMo: parseInt(process.env.PLAYWRIGHT_SLOWMO ?? '') || 0, + // force GPU hardware acceleration + // (even in headless mode) + args: ['--use-gl=egl'] + }, + permissions: ['local-network-access'] + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: {...devices['Desktop Chrome']} + }, + + ...(process.env.ALL_BROWSERS ? [{ + name: 'firefox', + use: {...devices['Desktop Firefox']} + }, + + { + name: 'webkit', + use: {...devices['Desktop Safari']} + }] : []) + ], + + /* Run local dev server before starting the tests */ + webServer: { + command: `pnpm dev:test`, + url: `http://localhost:${E2E_PORT}/signup-form.min.js`, + reuseExistingServer: !process.env.CI, + timeout: 10000 + } +}); diff --git a/apps/signup-form/postcss.config.cjs b/apps/signup-form/postcss.config.cjs new file mode 100644 index 0000000..ab7c493 --- /dev/null +++ b/apps/signup-form/postcss.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/apps/signup-form/preview.html b/apps/signup-form/preview.html new file mode 100644 index 0000000..37c78a3 --- /dev/null +++ b/apps/signup-form/preview.html @@ -0,0 +1,113 @@ + + + + + + Signup Form + + + +
+ +

Full signup form

+

+ Currently connected to Ghost running at %VITE_SITE_URL%. Please duplicate .env.development as .env.development.local and modify it to change the site url locally (when you get an error when submitting the forms). +

+ + +
+ +
+ +
+

Without icon

+
+ +
+ +
+

Minimal

+ +
+ +
+ +
+

With invalid configuration

+

When you submit this one, it will throw an error.

+ +
+ +
+ +
+

Translated

+ +
+ +
+ +
+ +
+ +
+
+ + diff --git a/apps/signup-form/src/app-context.ts b/apps/signup-form/src/app-context.ts new file mode 100644 index 0000000..73cb00b --- /dev/null +++ b/apps/signup-form/src/app-context.ts @@ -0,0 +1,34 @@ +// Ref: https://reactjs.org/docs/context.html +import React, {ComponentProps, useContext} from 'react'; +import pages, {Page, PageName} from './pages'; +import {GhostApi} from './utils/api'; + +export type SignupFormOptions = { + title?: string, + description?: string, + icon?: string, + backgroundColor?: string, + textColor?: string, + buttonColor?: string, + buttonTextColor?: string, + site: string, + labels: string[], + locale: string +}; + +export type AppContextType = { + page: Page, + setPage: (name: T, data: ComponentProps) => void, + options: SignupFormOptions, + api: GhostApi, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + t: any, + scriptTag: HTMLElement +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const AppContext = React.createContext({} as any); + +export const AppContextProvider = AppContext.Provider; + +export const useAppContext = () => useContext(AppContext); diff --git a/apps/signup-form/src/app.tsx b/apps/signup-form/src/app.tsx new file mode 100644 index 0000000..449e0c6 --- /dev/null +++ b/apps/signup-form/src/app.tsx @@ -0,0 +1,59 @@ +import React, {ComponentProps} from 'react'; +import i18nLib from '@tryghost/i18n'; +import pages, {Page, PageName} from './pages'; +import {AppContextProvider, AppContextType} from './app-context'; +import {ContentBox} from './components/content-box'; +import {Frame} from './components/frame'; +import {setupGhostApi} from './utils/api'; +import {useOptions} from './utils/options'; + +type AppProps = { + scriptTag: HTMLElement; +}; + +const App: React.FC = ({scriptTag}) => { + const options = useOptions(scriptTag); + + const [page, setPage] = React.useState({ + name: 'FormPage', + data: {} + }); + + const api = React.useMemo(() => { + return setupGhostApi({siteUrl: options.site}); + }, [options.site]); + + const _setPage = (name: T, data: ComponentProps) => { + setPage({ + name, + data + } as Page); + }; + + const i18n = i18nLib(options.locale, 'signup-form'); + const context: AppContextType = { + page, + api, + options, + setPage: _setPage, + t: i18n.t, + scriptTag + }; + + const PageComponent = pages[page.name]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = page.data as any; // issue with TypeScript understanding the type here when passing it to the component + return ( + <> + + + + + + + + + ); +}; + +export default App; diff --git a/apps/signup-form/src/i18n.d.ts b/apps/signup-form/src/i18n.d.ts new file mode 100644 index 0000000..36ee2a9 --- /dev/null +++ b/apps/signup-form/src/i18n.d.ts @@ -0,0 +1 @@ +declare module '@tryghost/i18n'; diff --git a/apps/signup-form/src/index.tsx b/apps/signup-form/src/index.tsx new file mode 100644 index 0000000..5d82352 --- /dev/null +++ b/apps/signup-form/src/index.tsx @@ -0,0 +1,54 @@ +import App from './app.tsx'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import {ROOT_DIV_CLASS} from './utils/constants'; + +function getScriptTag(): HTMLElement { + let scriptTag = document.currentScript as HTMLElement | null; + + if (!scriptTag && import.meta.env.DEV) { + // In development mode, use any script tag (because in ESM mode, document.currentScript is not set) + // We use the first script in the body element + scriptTag = document.querySelector('body script:not([data-used="true"])') as HTMLElement; + if (scriptTag) { + scriptTag.dataset.used = 'true'; + } + } + + if (!scriptTag) { + throw new Error('[Signup Form] Cannot find current script tag'); + } + + return scriptTag; +} + +/** + * Note that we need to support multiple signup forms on the same page, so we need to find the root div for each script tag + */ +function getRootDiv(scriptTag: HTMLElement) { + if (scriptTag.previousElementSibling && scriptTag.previousElementSibling.className === ROOT_DIV_CLASS) { + return scriptTag.previousElementSibling; + } + + if (!scriptTag.parentElement) { + throw new Error('[Signup Form] Script tag does not have a parent element'); + } + + const elem = document.createElement('div'); + elem.className = ROOT_DIV_CLASS; + scriptTag.parentElement.insertBefore(elem, scriptTag); + return elem; +} + +function init() { + const scriptTag = getScriptTag(); + const root = getRootDiv(scriptTag); + + ReactDOM.createRoot(root).render( + + + + ); +} + +init(); diff --git a/apps/signup-form/src/pages.tsx b/apps/signup-form/src/pages.tsx new file mode 100644 index 0000000..90e6e54 --- /dev/null +++ b/apps/signup-form/src/pages.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import {FormPage} from './components/pages/form-page'; +import {SuccessPage} from './components/pages/success-page'; + +const Pages = { + FormPage, + SuccessPage +}; + +export type PageName = keyof typeof Pages; + +type PageTypes = { + [name in PageName]: { + name: name, + data: React.ComponentProps + } +} + +export type Page = PageTypes[keyof PageTypes] + +export default Pages; diff --git a/apps/signup-form/src/preview.stories.tsx b/apps/signup-form/src/preview.stories.tsx new file mode 100644 index 0000000..7c037e0 --- /dev/null +++ b/apps/signup-form/src/preview.stories.tsx @@ -0,0 +1,129 @@ +import React, {useState} from 'react'; +import i18nLib from '@tryghost/i18n'; +import pages, {Page, PageName} from './pages'; +import {AppContextProvider, SignupFormOptions} from './app-context'; +import {ContentBox} from './components/content-box'; +import {userEvent, within} from '@storybook/testing-library'; +import type {Meta, StoryObj} from '@storybook/react'; + +type PreviewProps = SignupFormOptions & { + pageBackgroundColor: string; + simulateApiError: boolean; +}; + +const Preview: React.FC = ({simulateApiError, pageBackgroundColor, ...options}) => { + const [page, setPage] = useState({ + name: 'FormPage', + data: {} + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const _setPage = (name: PageName, data: any) => { + setPage(() => ({ + name, + data + })); + }; + + const PageComponent = pages[page.name]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = page.data as any; + + const i18n = i18nLib(options.locale || 'en', 'signup-form'); + + return { + // Sleep to ensure the loading state is visible enough + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + + if (simulateApiError) { + throw new Error('API Error'); + } + + return; + }, + getIntegrityToken: async () => { + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + + return 'testtoken'; + } + }, + t: i18n.t, + options, + scriptTag: document.createElement('div') + }}> +
+ + + +
+
; +}; + +const meta = { + title: 'Preview', + component: Preview, + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + const emailInput = canvas.getByTestId('input'); + + await userEvent.type(emailInput, 'test@example.com', { + delay: 100 + }); + + const submitButton = canvas.getByTestId('button'); + userEvent.click(submitButton); + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Full: Story = { + args: { + title: 'Signup Forms Weekly', + description: 'An independent publication about embeddable signup forms.', + icon: 'https://user-images.githubusercontent.com/65487235/157884383-1b75feb1-45d8-4430-b636-3f7e06577347.png', + backgroundColor: '#eeeeee', + textColor: '#000000', + buttonColor: '#ff0095', + buttonTextColor: '#ffffff', + site: 'localhost', + labels: ['label-1', 'label-2'], + simulateApiError: false, + pageBackgroundColor: '#ffffff', + locale: 'en' + } +}; + +export const Minimal: Story = { + args: { + site: 'localhost', + labels: ['label-1', 'label-2'], + buttonColor: '#ff0095', + buttonTextColor: '#ffffff', + simulateApiError: false, + pageBackgroundColor: '#ffffff', + locale: 'en' + } +}; + +export const MinimalOnDark: Story = { + args: { + site: 'localhost', + labels: ['label-1', 'label-2'], + buttonColor: '#ff0095', + buttonTextColor: '#ffffff', + simulateApiError: false, + pageBackgroundColor: '#122334', + locale: 'en' + } +}; diff --git a/apps/signup-form/src/typings.d.ts b/apps/signup-form/src/typings.d.ts new file mode 100644 index 0000000..f1ae460 --- /dev/null +++ b/apps/signup-form/src/typings.d.ts @@ -0,0 +1,7 @@ +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-require-imports + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; + } diff --git a/apps/signup-form/src/vite-env.d.ts b/apps/signup-form/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/signup-form/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/signup-form/tailwind.config.cjs b/apps/signup-form/tailwind.config.cjs new file mode 100644 index 0000000..347d509 --- /dev/null +++ b/apps/signup-form/tailwind.config.cjs @@ -0,0 +1,193 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + corePlugins: { + preflight: true + }, + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + screens: { + xs: '439px', + sm: '480px', + md: '640px', + lg: '1024px', + xl: '1280px' + }, + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#FFF', + black: '#15171A', + grey: { + DEFAULT: '#ABB4BE', + 50: '#FAFAFB', + 100: '#F4F5F6', + 200: '#EBEEF0', + 300: '#DDE1E5', + 400: '#CED4D9', + 500: '#AEB7C1', + 600: '#95A1AD', + 700: '#7C8B9A', + 800: '#626D79', + 900: '#394047' + }, + green: { + DEFAULT: '#30CF43', + 100: '#E1F9E4', + 400: '#58DA67', + 500: '#30CF43', + 600: '#2AB23A' + }, + blue: { + DEFAULT: '#14B8FF', + 100: '#DBF4FF', + 400: '#42C6FF', + 500: '#14B8FF', + 600: '#00A4EB' + }, + purple: { + DEFAULT: '#8E42FF', + 100: '#EDE0FF', + 400: '#A366FF', + 500: '#8E42FF', + 600: '7B1FFF' + }, + pink: { + DEFAULT: '#FB2D8D', + 100: '#FFDFEE', + 400: '#FF5CA8', + 500: '#FB2D8D', + 600: '#F70878' + }, + red: { + DEFAULT: '#F50B23', + 100: '#FFE0E0', + 400: '#F9394C', + 500: '#F50B23', + 600: '#DC091E' + }, + yellow: { + DEFAULT: '#FFB41F', + 100: '#FFF1D6', + 400: '#FFC247', + 500: '#FFB41F', + 600: '#F0A000' + }, + lime: { + DEFAULT: '#B5FF18' + } + }, + fontFamily: { + inter: 'Inter', + sans: 'Inter, -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif', + serif: 'Georgia, serif', + mono: 'Consolas, Liberation Mono, Menlo, Courier, monospace' + }, + boxShadow: { + DEFAULT: '0 0 1px rgba(0,0,0,.05), 0 5px 18px rgba(0,0,0,.08)', + sm: '0 0 1px rgba(0,0,0,.12), 0 1px 6px rgba(0,0,0,0.03), 0 6px 10px -8px rgba(0,0,0,.1)', + md: '0 0 1px rgba(0,0,0,.05), 0 8px 28px rgba(0,0,0,.12)', + lg: '0 0 7px rgba(0, 0, 0, 0.08), 0 2.1px 2.2px -5px rgba(0, 0, 0, 0.011), 0 5.1px 5.3px -5px rgba(0, 0, 0, 0.016), 0 9.5px 10px -5px rgba(0, 0, 0, 0.02), 0 17px 17.9px -5px rgba(0, 0, 0, 0.024), 0 31.8px 33.4px -5px rgba(0, 0, 0, 0.029), 0 76px 80px -5px rgba(0, 0, 0, 0.04)', + xl: '0 2.8px 2.2px rgba(0, 0, 0, 0.02), 0 6.7px 5.3px rgba(0, 0, 0, 0.028), 0 12.5px 10px rgba(0, 0, 0, 0.035), 0 22.3px 17.9px rgba(0, 0, 0, 0.042), 0 41.8px 33.4px rgba(0, 0, 0, 0.05), 0 100px 80px rgba(0, 0, 0, 0.07)', + inner: 'inset 0 0 4px 0 rgb(0 0 0 / 0.08)', + none: '0 0 #0000' + }, + extend: { + spacing: { + px: '1px', + 0: '0px', + 0.5: '0.2rem', + 1: '0.4rem', + 1.5: '0.6rem', + 2: '0.8rem', + 2.5: '1rem', + 3: '1.2rem', + 3.5: '1.4rem', + 4: '1.6rem', + 5: '2rem', + 6: '2.4rem', + 7: '2.8rem', + 8: '3.2rem', + 9: '3.6rem', + 10: '4rem', + 11: '4.4rem', + 12: '4.8rem', + 14: '5.6rem', + 16: '6.4rem', + 18: '7.2rem', + 20: '8rem', + 24: '9.6rem', + 28: '11.2rem', + 32: '12.8rem', + 36: '14.4rem', + 40: '16rem', + 44: '17.6rem', + 48: '19.2rem', + 52: '20.8rem', + 56: '22.4rem', + 60: '24rem', + 64: '25.6rem', + 72: '28.8rem', + 80: '32rem', + 96: '38.4rem' + }, + maxWidth: { + none: 'none', + 0: '0rem', + xs: '32rem', + sm: '38.4rem', + md: '44.8rem', + lg: '51.2rem', + xl: '57.6rem', + '2xl': '67.2rem', + '3xl': '76.8rem', + '4xl': '89.6rem', + '5xl': '102.4rem', + '6xl': '115.2rem', + '7xl': '128rem', + '8xl': '140rem', + '9xl': '156rem', + full: '100%', + min: 'min-content', + max: 'max-content', + fit: 'fit-content', + prose: '65ch' + }, + borderRadius: { + sm: '0.3rem', + DEFAULT: '0.4rem', + md: '0.6rem', + lg: '0.8rem', + xl: '1.2rem', + '2xl': '1.6rem', + '3xl': '2.4rem', + full: '9999px' + }, + fontSize: { + '2xs': '1.05rem', + base: '1.5rem', + xs: '1.2rem', + sm: '1.35rem', + md: '1.5rem', + lg: '1.8rem', + xl: '2rem', + '2xl': '2.4rem', + '3xl': '3rem', + '4xl': '3.6rem', + '5xl': ['4.2rem', '1.15'], + '6xl': ['6rem', '1'], + '7xl': ['7.2rem', '1'], + '8xl': ['9.6rem', '1'], + '9xl': ['12.8rem', '1'] + }, + lineHeight: { + base: '1.5em', + tight: '1.35em', + tighter: '1.25em', + supertight: '1.1em' + }, + transition: { + basic: 'all 0.4 ease' + } + } + } +}; diff --git a/apps/signup-form/tsconfig.json b/apps/signup-form/tsconfig.json new file mode 100644 index 0000000..0744386 --- /dev/null +++ b/apps/signup-form/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/apps/signup-form/tsconfig.node.json b/apps/signup-form/tsconfig.node.json new file mode 100644 index 0000000..c5e36d4 --- /dev/null +++ b/apps/signup-form/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.mts", "package.json"] +} diff --git a/apps/signup-form/vite.config.mts b/apps/signup-form/vite.config.mts new file mode 100644 index 0000000..89bbe0d --- /dev/null +++ b/apps/signup-form/vite.config.mts @@ -0,0 +1,73 @@ +import pkg from './package.json'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; +import {SUPPORTED_LOCALES} from '@tryghost/i18n'; +import {defineConfig} from 'vitest/config'; +import {resolve} from 'path'; + +const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name; + +// https://vitejs.dev/config/ +export default (function viteConfig() { + return defineConfig({ + logLevel: process.env.CI ? 'info' : 'warn', + plugins: [ + svgr(), + react() + ], + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.VITEST_SEGFAULT_RETRY': 3 + }, + preview: { + host: '0.0.0.0', + allowedHosts: true, // allows domain-name proxies to the preview server + port: 6174 + }, + optimizeDeps: { + include: ['@tryghost/i18n', '@tryghost/debug'] + }, + resolve: { + dedupe: ['@tryghost/debug'] + }, + build: { + outDir: resolve(__dirname, 'umd'), + reportCompressedSize: false, + emptyOutDir: true, + minify: true, + sourcemap: true, + cssCodeSplit: true, + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + formats: ['umd'], + name: pkg.name, + fileName(format) { + if (format === 'umd') { + return `${outputFileName}.min.js`; + } + + return `${outputFileName}.js`; + } + }, + rollupOptions: { + output: {} + }, + commonjsOptions: { + include: [/ghost/, /node_modules/], + dynamicRequireRoot: '../../', + dynamicRequireTargets: SUPPORTED_LOCALES.map(locale => `../../ghost/i18n/locales/${locale}/signup-form.json`) + } + }, + test: { + globals: true, // required for @testing-library/jest-dom extensions + environment: 'jsdom', + setupFiles: './test/test-setup.js', + include: ['./test/unit/*'], + testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000, + ...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674 + minThreads: 1, + maxThreads: 2 + }) + } + }); +}); diff --git a/apps/sodo-search/LICENSE b/apps/sodo-search/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/apps/sodo-search/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/sodo-search/README.md b/apps/sodo-search/README.md new file mode 100644 index 0000000..bad3b42 --- /dev/null +++ b/apps/sodo-search/README.md @@ -0,0 +1,39 @@ +# Sodo Search + +## Development + +### Pre-requisites + +- Run `pnpm` in Ghost monorepo root +- Run `pnpm` in this directory + +### Running via Ghost `pnpm dev` in root folder + +Sodo Search runs automatically when using Ghost's development command from the monorepo root: +```bash +pnpm dev +``` + +This starts all frontend apps (including Sodo Search.) + +## Release + +A patch release can be rolled out instantly in production, whereas a minor/major release requires the Ghost monorepo to be updated and released. +In either case, you need sufficient permissions to release `@tryghost` packages on NPM. + +### Patch release + +1. Run `pnpm ship` and select a patch version when prompted +2. Merge the release commit to `main` + +### Minor / major release + +1. Run `pnpm ship` and select a minor or major version when prompted +2. Merge the release commit to `main` +3. Wait until a new version of Ghost is released + +To use the new version of Sodo-Search in Ghost, update the version in Ghost core's default configuration (currently at `core/shared/config/default.json`) + +# Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/apps/sodo-search/package.json b/apps/sodo-search/package.json new file mode 100644 index 0000000..c8d1d24 --- /dev/null +++ b/apps/sodo-search/package.json @@ -0,0 +1,110 @@ +{ + "name": "@tryghost/sodo-search", + "version": "1.8.11", + "license": "MIT", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "files": [ + "umd/", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "@tryghost/debug": "0.1.40", + "@tryghost/i18n": "workspace:*", + "flexsearch": "0.8.153", + "react": "17.0.2", + "react-dom": "17.0.2" + }, + "scripts": { + "dev": "concurrently \"vite preview -l silent\" \"pnpm build:watch\" \"pnpm tailwind\"", + "build": "vite build && pnpm tailwind:base", + "build:watch": "vite build --watch", + "tailwind": "pnpm tailwind:base --watch ", + "tailwind:base": "tailwindcss -i ./src/index.css -o ./umd/main.css --minify", + "test": "vitest run", + "test:ci": "pnpm test --coverage", + "test:unit": "pnpm test:ci", + "lint": "eslint src --ext .js --cache", + "preship": "pnpm lint", + "ship": "node ../../.github/scripts/release-apps.js", + "prepublishOnly": "pnpm build" + }, + "eslintConfig": { + "env": { + "browser": true, + "jest": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2022 + }, + "extends": [ + "plugin:ghost/browser", + "plugin:react/recommended" + ], + "plugins": [ + "ghost" + ], + "rules": { + "react/prop-types": "off", + "ghost/filenames/match-regex": [ + "error", + "^[a-z0-9.-]+$", + false + ], + "ghost/sort-imports-es6-autofix/sort-imports-es6": [ + "error", + { + "memberSyntaxSortOrder": [ + "none", + "all", + "single", + "multiple" + ] + } + ] + }, + "settings": { + "react": { + "version": "detect" + } + } + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "jest": { + "coverageReporters": [ + "cobertura", + "text-summary", + "html" + ] + }, + "devDependencies": { + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "12.1.5", + "@vitejs/plugin-react": "4.7.0", + "@vitest/coverage-v8": "~3.2.4", + "cross-fetch": "4.1.0", + "jsdom": "28.1.0", + "nock": "13.5.6", + "tailwindcss": "3.4.18", + "vite": "5.4.21", + "vite-plugin-svgr": "3.3.0", + "vitest": "3.2.4" + } +} diff --git a/apps/sodo-search/src/app-context.js b/apps/sodo-search/src/app-context.js new file mode 100644 index 0000000..801ef02 --- /dev/null +++ b/apps/sodo-search/src/app-context.js @@ -0,0 +1,21 @@ +// Ref: https://reactjs.org/docs/context.html +import React from 'react'; + +const AppContext = React.createContext({ + posts: [], + authors: [], + tags: [], + action: '', + lastPage: '', + page: '', + pageData: {}, + // eslint-disable-next-line no-unused-vars + dispatch: (_action, _data) => {}, + searchIndex: null, + indexComplete: false, + searchValue: '', + t: () => {}, + dir: 'ltr' +}); + +export default AppContext; diff --git a/apps/sodo-search/src/app.css b/apps/sodo-search/src/app.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/apps/sodo-search/src/app.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/apps/sodo-search/src/app.js b/apps/sodo-search/src/app.js new file mode 100644 index 0000000..0feb0db --- /dev/null +++ b/apps/sodo-search/src/app.js @@ -0,0 +1,212 @@ +import './app.css'; +import AppContext from './app-context'; +import PopupModal from './components/popup-modal'; +import React from 'react'; +import SearchIndex from './search-index.js'; +import i18nLib from '@tryghost/i18n'; + +export default class App extends React.Component { + constructor(props) { + super(props); + + const i18nLanguage = this.props.locale || 'en'; + const i18n = i18nLib(i18nLanguage, 'search'); + const dir = i18n.dir() || 'ltr'; + + const searchIndex = new SearchIndex({ + adminUrl: props.adminUrl, + apiKey: props.apiKey, + dir: dir + }); + + this.state = { + searchIndex, + showPopup: false, + indexStarted: false, + indexComplete: false, + t: i18n.t, + dir: dir, + scrollbarWidth: 0 + }; + + this.inputRef = React.createRef(); + } + + componentDidMount() { + const scrollbarWidth = this.getScrollbarWidth(); + this.setState({scrollbarWidth}); + + this.initSetup(); + } + + componentDidUpdate(_prevProps, prevState) { + if (prevState.showPopup !== this.state.showPopup) { + /** Remove background scroll when popup is opened */ + try { + if (this.state.showPopup) { + /** When modal is opened, store current overflow and set as hidden */ + this.bodyScroll = window.document?.body?.style?.overflow; + this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right'); + window.document.body.style.overflow = 'hidden'; + if (this.state.scrollbarWidth && document.body.scrollHeight > window.innerHeight) { + window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`; + } + } else { + /** When the modal is hidden, reset overflow property for body */ + window.document.body.style.overflow = this.bodyScroll || ''; + if (!this.bodyMargin || this.bodyMargin === '0px') { + window.document.body.style.marginRight = ''; + } else { + window.document.body.style.marginRight = this.bodyMargin; + } + } + } catch (e) { + /** Ignore any errors for scroll handling */ + } + } + + if (this.state.showPopup !== prevState?.showPopup && !this.state.showPopup) { + this.setState({ + searchValue: '' + }); + } + + if (this.state.showPopup && !this.state.indexStarted) { + this.setupSearchIndex(); + } + } + + async setupSearchIndex() { + this.setState({ + indexStarted: true + }); + await this.state.searchIndex.init(); + this.setState({ + indexComplete: true + }); + } + + componentWillUnmount() { + /**Clear timeouts and event listeners on unmount */ + window.removeEventListener('hashchange', this.hashHandler, false); + window.removeEventListener('keydown', this.handleKeyDown, false); + } + + initSetup() { + // Listen to preview mode changes + this.handleSearchUrl(); + this.addKeyboardShortcuts(); + this.setupCustomTriggerButton(); + this.hashHandler = () => { + this.handleSearchUrl(); + }; + window.addEventListener('hashchange', this.hashHandler, false); + } + + // User for adding trailing margin to prevent layout shift when popup appears + getScrollbarWidth() { + // Create a temporary div + const div = document.createElement('div'); + div.style.visibility = 'hidden'; + div.style.overflow = 'scroll'; // forcing scrollbar to appear + document.body.appendChild(div); + + // Calculate the width difference + const scrollbarWidth = div.offsetWidth - div.clientWidth; + + // Clean up + document.body.removeChild(div); + + return scrollbarWidth; + } + + /** Setup custom trigger buttons handling on page */ + setupCustomTriggerButton() { + // Handler for custom buttons + this.clickHandler = (event) => { + event.preventDefault(); + this.setState({ + showPopup: true + }); + + const tmpElement = document.createElement('input'); + tmpElement.style.opacity = '0'; + tmpElement.style.position = 'fixed'; + tmpElement.style.top = '0'; + document.body.appendChild(tmpElement); + tmpElement.focus(); + + setTimeout(() => { + this.inputRef.current.focus(); + document.body.removeChild(tmpElement); + }, 150); + }; + + this.customTriggerButtons = this.getCustomTriggerButtons(); + this.customTriggerButtons.forEach((customTriggerButton) => { + customTriggerButton.removeEventListener('click', this.clickHandler); + customTriggerButton.addEventListener('click', this.clickHandler); + }); + } + + getCustomTriggerButtons() { + const customTriggerSelector = '[data-ghost-search]'; + return document.querySelectorAll(customTriggerSelector) || []; + } + + handleSearchUrl() { + const [path] = window.location.hash.substr(1).split('?'); + if (path === '/search' || path === '/search/') { + this.setState({ + showPopup: true + }); + window.history.replaceState('', document.title, window.location.pathname); + } + } + + addKeyboardShortcuts() { + const customTriggerButtons = this.getCustomTriggerButtons(); + if (!customTriggerButtons?.length) { + return; + } + this.handleKeyDown = (e) => { + if (e.key === 'k' && e.metaKey) { + this.setState({ + showPopup: true + }); + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + document.addEventListener('keydown', this.handleKeyDown); + } + + render() { + return ( + {}, + dispatch: (action, data) => { + if (action === 'update') { + this.setState({ + ...this.state, + ...data + }); + } + }, + t: this.state.t, + dir: this.state.dir + }}> + + + ); + } +} diff --git a/apps/sodo-search/src/index.css b/apps/sodo-search/src/index.css new file mode 100644 index 0000000..9e90d54 --- /dev/null +++ b/apps/sodo-search/src/index.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Custom reset */ +html { + font-size: 62.5%; +} + +body { + font-size: 1.5rem; +} + +.ghost-display { + display: block !important; +} diff --git a/apps/sodo-search/src/index.js b/apps/sodo-search/src/index.js new file mode 100644 index 0000000..192b3e5 --- /dev/null +++ b/apps/sodo-search/src/index.js @@ -0,0 +1,48 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './app'; + +const ROOT_DIV_ID = 'sodo-search-root'; + +function addRootDiv() { + const elem = document.createElement('div'); + elem.id = ROOT_DIV_ID; + document.body.appendChild(elem); +} + +function getSiteData() { + /** + * @type {HTMLElement} + */ + const scriptTag = document.querySelector('script[data-sodo-search]'); + if (scriptTag) { + const adminUrl = scriptTag.dataset.sodoSearch; + const apiKey = scriptTag.dataset.key; + const stylesUrl = scriptTag.dataset.styles; + const locale = scriptTag.dataset.locale || 'en'; + return {adminUrl, apiKey, stylesUrl, locale}; + } + return {}; +} + +function setup() { + addRootDiv(); +} + +function init() { + const {adminUrl, apiKey, stylesUrl, locale} = getSiteData(); + const adminBaseUrl = (adminUrl || window.location.origin)?.replace(/\/+$/, ''); + setup(); + ReactDOM.render( + + + , + document.getElementById(ROOT_DIV_ID) + ); +} + +init(); diff --git a/apps/sodo-search/src/logo.svg b/apps/sodo-search/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/apps/sodo-search/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/sodo-search/src/search-index.js b/apps/sodo-search/src/search-index.js new file mode 100644 index 0000000..237c51e --- /dev/null +++ b/apps/sodo-search/src/search-index.js @@ -0,0 +1,227 @@ +import Flexsearch, {Charset} from 'flexsearch'; + +const cjkEncoderPresetCodepoint = { + finalize: (terms) => { + let results = []; + + for (const term of terms) { + results.push(...tokenizeCjkByCodePoint(term)); + } + return results; + } +}; + +function isCJK(codePoint) { + return ( + (codePoint >= 0x4E00 && codePoint <= 0x9FFF) || // CJK Unified Ideographs + (codePoint >= 0x3040 && codePoint <= 0x30FF) || // Hiragana & Katakana (contiguous blocks) + (codePoint >= 0xAC00 && codePoint <= 0xD7A3) || // Korean Hangul Syllables + (codePoint >= 0x3400 && codePoint <= 0x4DBF) || // CJK Unified Ideographs Extension A + (codePoint >= 0x20000 && codePoint <= 0x2A6DF) || // CJK Unified Ideographs Extension B + (codePoint >= 0x2A700 && codePoint <= 0x2EBEF) || // CJK Unified Ideographs Extension C-F (contiguous blocks) + (codePoint >= 0x30000 && codePoint <= 0x323AF) || // Additional ideographs + (codePoint >= 0x2EBF0 && codePoint <= 0x2EE5F) || // More extensions + (codePoint >= 0xF900 && codePoint <= 0xFAFF) || // Compatibility Ideographs + (codePoint >= 0x2F800 && codePoint <= 0x2FA1F) // Supplementary ideographs + ); +} + +export function tokenizeCjkByCodePoint(text) { + const result = []; + let buffer = ''; + + for (const char of text) { // loops over unicode characters + const codePoint = char.codePointAt(0); + + if (isCJK(codePoint)) { + if (buffer) { + result.push(buffer); // Push any non-CJK word we’ve been building + buffer = ''; + } + result.push(char); // Push the CJK char as its own token + } else { + buffer += char; // Keep building non-CJK text + } + } + + if (buffer) { + result.push(buffer); // Push whatever is left when done + } + + return result; +} + +const encoderSet = new Flexsearch.Encoder( + Charset.Default, + cjkEncoderPresetCodepoint +); + +export default class SearchIndex { + constructor({adminUrl, apiKey, dir}) { + const rtl = (dir === 'rtl'); + const tokenize = (dir === 'rtl') ? 'reverse' : 'forward'; + + this.apiUrl = adminUrl; + this.apiKey = apiKey; + + this.postsIndex = new Flexsearch.Document({ + tokenize: tokenize, + rtl: rtl, + document: { + id: 'id', + index: ['title', 'excerpt'], + store: true + }, + encoder: encoderSet + }); + + this.authorsIndex = new Flexsearch.Document({ + tokenize: tokenize, + rtl: rtl, + document: { + id: 'id', + index: ['name'], + store: true + }, + encoder: encoderSet + }); + + this.tagsIndex = new Flexsearch.Document({ + tokenize: tokenize, + rtl: rtl, + document: { + id: 'id', + index: ['name'], + store: true + }, + encoder: encoderSet + }); + + this.init = this.init.bind(this); + this.search = this.search.bind(this); + } + + async #populatePostIndex() { + const posts = await this.#fetchPosts(); + + if (posts.length > 0) { + this.#updatePostIndex(posts); + } + } + + async #fetchPosts() { + try { + const url = `${this.apiUrl}/ghost/api/content/search-index/posts/?key=${this.apiKey}`; + const response = await fetch(url); + const json = await response.json(); + + return json.posts; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching posts:', error); + return []; + } + } + + #updatePostIndex(posts) { + posts.forEach((post) => { + this.postsIndex.add(post); + }); + } + + async #populateAuthorsIndex() { + const authors = await this.#fetchAuthors(); + + if (authors.length > 0) { + this.#updateAuthorsIndex(authors); + } + } + + async #fetchAuthors() { + try { + const url = `${this.apiUrl}/ghost/api/content/search-index/authors/?key=${this.apiKey}`; + const response = await fetch(url); + const json = await response.json(); + + return json.authors; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching authors:', error); + return []; + } + } + + #updateAuthorsIndex(authors) { + authors.forEach((author) => { + this.authorsIndex.add(author); + }); + } + + async #populateTagsIndex() { + const tags = await this.#fetchTags(); + + if (tags.length > 0) { + this.#updateTagsIndex(tags); + } + } + + async #fetchTags() { + try { + const url = `${this.apiUrl}/ghost/api/content/search-index/tags/?key=${this.apiKey}`; + const response = await fetch(url); + const json = await response.json(); + + return json.tags; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching tags:', error); + return []; + } + } + + #updateTagsIndex(tags) { + tags.forEach((tag) => { + this.tagsIndex.add(tag); + }); + } + + async init() { + await this.#populatePostIndex(); + await this.#populateAuthorsIndex(); + await this.#populateTagsIndex(); + } + + #normalizeSearchResult(result) { + const normalized = []; + const usedIds = {}; + + result.forEach((resultItem) => { + resultItem.result.forEach((doc) => { + if (!usedIds[doc.id]) { + normalized.push(doc.doc); + usedIds[doc.id] = true; + } + }); + }); + + return normalized; + } + + search(value) { + const posts = this.postsIndex.search(value, { + enrich: true + }); + const authors = this.authorsIndex.search(value, { + enrich: true + }); + const tags = this.tagsIndex.search(value, { + enrich: true + }); + + return { + posts: this.#normalizeSearchResult(posts), + authors: this.#normalizeSearchResult(authors), + tags: this.#normalizeSearchResult(tags) + }; + } +} diff --git a/apps/sodo-search/tailwind.config.js b/apps/sodo-search/tailwind.config.js new file mode 100644 index 0000000..31a5eff --- /dev/null +++ b/apps/sodo-search/tailwind.config.js @@ -0,0 +1,116 @@ +/** @type {import('tailwindcss').Config} */ + +module.exports = { + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1400px' + }, + spacing: { + px: '1px', + 0: '0px', + 0.5: '0.2rem', + 1: '0.4rem', + 1.5: '0.6rem', + 2: '0.8rem', + 2.5: '1rem', + 3: '1.2rem', + 3.5: '1.4rem', + 4: '1.6rem', + 5: '2rem', + 6: '2.4rem', + 7: '2.8rem', + 8: '3.2rem', + 9: '3.6rem', + 10: '4rem', + 11: '4.4rem', + 12: '4.8rem', + 14: '5.6rem', + 16: '6.4rem', + 20: '8rem', + 24: '9.6rem', + 28: '11.2rem', + 32: '12.8rem', + 36: '14.4rem', + 40: '16rem', + 44: '17.6rem', + 48: '19.2rem', + 52: '20.8rem', + 56: '22.4rem', + 60: '24rem', + 64: '25.6rem', + 72: '28.8rem', + 80: '32rem', + 96: '38.4rem' + }, + maxWidth: { + none: 'none', + 0: '0rem', + xs: '32rem', + sm: '38.4rem', + md: '44.8rem', + lg: '51.2rem', + xl: '57.6rem', + '2xl': '67.2rem', + '3xl': '76.8rem', + '4xl': '89.6rem', + '5xl': '102.4rem', + '6xl': '115.2rem', + '7xl': '128rem', + '8xl': '140rem', + '9xl': '156rem', + full: '100%', + min: 'min-content', + max: 'max-content', + fit: 'fit-content', + prose: '65ch' + }, + borderRadius: { + sm: '0.2rem', + DEFAULT: '0.4rem', + md: '0.6rem', + lg: '0.8rem', + xl: '1.2rem', + '2xl': '1.6rem', + '3xl': '2.4rem', + full: '9999px' + }, + fontSize: { + xs: '1.2rem', + sm: '1.4rem', + md: '1.5rem', + lg: '1.8rem', + xl: '2rem', + '2xl': '2.4rem', + '3xl': '3rem', + '4xl': '3.6rem', + '5xl': ['4.8rem', '1.15'], + '6xl': ['6rem', '1'], + '7xl': ['7.2rem', '1'], + '8xl': ['9.6rem', '1'], + '9xl': ['12.8rem', '1'] + }, + animation: { + 'popup': 'popup 0.15s ease', + 'fadein': 'fadein 0.15s' + }, + keyframes: { + popup: { + '0%': { transform: 'translateY(-20px)', opacity: '0' }, + '1%': { transform: 'translateY(20px)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1.0' } + }, + fadein: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' } + } + } + }, + content: [ + './src/**/*.{js,jsx,ts,tsx}' + ], + plugins: [] +}; \ No newline at end of file diff --git a/apps/sodo-search/test/setup-tests.js b/apps/sodo-search/test/setup-tests.js new file mode 100644 index 0000000..d835694 --- /dev/null +++ b/apps/sodo-search/test/setup-tests.js @@ -0,0 +1,20 @@ +import matchers from '@testing-library/jest-dom/matchers'; +import {afterEach, expect} from 'vitest'; +import {cleanup} from '@testing-library/react'; +import {fetch} from 'cross-fetch'; + +// TODO: remove this once we're switched `jest` to `vi` in code +// eslint-disable-next-line no-undef +globalThis.jest = vi; + +// eslint-disable-next-line no-undef +globalThis.fetch = fetch; + +// Add the cleanup function for React testing library +afterEach(cleanup); + +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +expect.extend(matchers); diff --git a/apps/sodo-search/vite.config.mjs b/apps/sodo-search/vite.config.mjs new file mode 100644 index 0000000..6338960 --- /dev/null +++ b/apps/sodo-search/vite.config.mjs @@ -0,0 +1,78 @@ +/* eslint-env node */ +import {resolve} from 'path'; +import fs from 'fs/promises'; + +import {defineConfig} from 'vitest/config'; +import reactPlugin from '@vitejs/plugin-react'; +import svgrPlugin from 'vite-plugin-svgr'; + +import pkg from './package.json'; +import {SUPPORTED_LOCALES} from '@tryghost/i18n'; +export default defineConfig((config) => { + const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name; + + return { + logLevel: process.env.CI ? 'info' : 'warn', + clearScreen: false, + define: { + 'process.env.NODE_ENV': JSON.stringify(config.mode) + }, + preview: { + host: '0.0.0.0', + allowedHosts: true, // allows domain-name proxies to the preview server + port: 4178 + }, + plugins: [ + reactPlugin(), + svgrPlugin() + ], + esbuild: { + loader: 'jsx', + include: /(src|test)\/.*\.jsx?$/, + exclude: [] + }, + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'load-js-files-as-jsx', + setup(build) { + build.onLoad({filter: /(src|test)\/.*\.js$/}, async args => ({ + loader: 'jsx', + contents: await fs.readFile(args.path, 'utf8') + })); + } + } + ] + } + }, + resolve: { + dedupe: ['@tryghost/debug'] + }, + build: { + outDir: resolve(__dirname, 'umd'), + reportCompressedSize: false, + emptyOutDir: true, + minify: true, + sourcemap: true, + cssCodeSplit: true, + lib: { + entry: resolve(__dirname, 'src/index.js'), + formats: ['umd'], + name: pkg.name, + fileName: format => `${outputFileName}.min.js` + }, + commonjsOptions: { + include: [/ghost/, /node_modules/], + dynamicRequireRoot: '../../', + dynamicRequireTargets: SUPPORTED_LOCALES.map(locale => `../../ghost/i18n/locales/${locale}/search.json`) + } + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './test/setup-tests.js', + testTimeout: 10000 + } + }; +}); diff --git a/apps/stats/.env.example b/apps/stats/.env.example new file mode 100644 index 0000000..97a7f56 --- /dev/null +++ b/apps/stats/.env.example @@ -0,0 +1,5 @@ +STATS_ENDPOINT=https://api.tinybird.co +STATS_TOKEN=p.ey.... +STATS_LOCAL_ENDPOINT=http://localhost:7181 +STATS_LOCAL_TOKEN=p.ey... +STATS_LOCAL_DATASOURCE=analytics_events diff --git a/apps/stats/.eslintignore b/apps/stats/.eslintignore new file mode 100644 index 0000000..9944ecc --- /dev/null +++ b/apps/stats/.eslintignore @@ -0,0 +1 @@ +tailwind.config.cjs diff --git a/apps/stats/.eslintrc.cjs b/apps/stats/.eslintrc.cjs new file mode 100644 index 0000000..df7a10d --- /dev/null +++ b/apps/stats/.eslintrc.cjs @@ -0,0 +1,70 @@ +/* eslint-env node */ +const tailwindCssConfig = `${__dirname}/../admin/src/index.css`; + +module.exports = { + root: true, + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + }, + tailwindcss: { + config: tailwindCssConfig + } + }, + rules: { + // Sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + 'no-restricted-imports': ['error', { + paths: [{ + name: '@tryghost/shade', + message: 'Import from layered subpaths instead (components/primitives/patterns/utils/app/tokens).' + }] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // TODO: re-enable this (maybe fixed fast refresh?) + 'react-refresh/only-export-components': 'off', + + // Suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // Ignore prop-types for now + 'react/prop-types': 'off', + + // TODO: re-enable these if deemed useful + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-function': 'off', + + // custom react rules + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + 'tailwindcss/classnames-order': 'error', + 'tailwindcss/enforces-negative-arbitrary-values': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/migration-from-tailwind-2': 'warn', + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': 'error' + } +}; diff --git a/apps/stats/.gitignore b/apps/stats/.gitignore new file mode 100644 index 0000000..0f817cd --- /dev/null +++ b/apps/stats/.gitignore @@ -0,0 +1,4 @@ +dist +types +playwright-report +test-results diff --git a/apps/stats/README.md b/apps/stats/README.md new file mode 100644 index 0000000..f41b982 --- /dev/null +++ b/apps/stats/README.md @@ -0,0 +1,93 @@ +# Ghost Stats App + +Ghost Admin Stats micro-frontend that provides analytics and insights for Ghost sites. + +## Features + +### Top Content Analytics +- **Growth Tab**: Shows which posts and pages drove the most member conversions +- **Web Tab**: Shows which posts and pages received the most visitors + +### URL Linking +All content in the analytics tables is now clickable: +- **Posts**: Click to view detailed post analytics +- **Pages**: Click to view the page on the frontend site +- **System Pages**: Click to view homepage, tag pages, author pages, etc. on the frontend site + +The app automatically determines the appropriate action: +- Posts with analytics data → Navigate to post analytics page +- Pages and system pages → Open frontend URL in new tab + +### Supported System Pages +- Homepage (`/`) +- Tag pages (`/tag/slug/`, `/tags/slug/`) +- Author pages (`/author/slug/`, `/authors/slug/`) +- Custom pages and other frontend URLs + +## Development + +### Prerequisites + +- Node.js (version as specified in the root package.json) +- Yarn + +### Setup + +This app is part of the Ghost monorepo. After cloning the Ghost repository: + +```bash +# Install dependencies from the root directory +pnpm + +# Run pnpm dev in the root of the repo +pnpm dev +``` + +### Build + +```bash +pnpm build +``` + +This will create a production build in the `dist` directory. + +### Testing + +```bash +# Run all tests +pnpm test + +# Run only unit tests +pnpm test:unit + +# Run tests in watch mode during development +pnpm test:watch + +# Run tests with coverage report +pnpm test:coverage +``` + +### Linting + +```bash +# Lint all files +pnpm lint + +# Lint only source code +pnpm lint:code + +# Lint only test files +pnpm lint:test +``` + +## License + +MIT - See LICENSE file for details. + +## URL Utilities + +The app includes URL helper utilities in `src/utils/url-helpers.ts`: + +- `getFrontendUrl()`: Generate full frontend URLs from attribution paths +- `shouldMakeClickable()`: Determine if content should be clickable +- `getClickHandler()`: Get appropriate click handler for content type diff --git a/apps/stats/index.html b/apps/stats/index.html new file mode 100644 index 0000000..975a8dc --- /dev/null +++ b/apps/stats/index.html @@ -0,0 +1,16 @@ + + + + + + + + Ghost Traffic Analytics + + + +
+ + + + \ No newline at end of file diff --git a/apps/stats/package.json b/apps/stats/package.json new file mode 100644 index 0000000..84ff7e0 --- /dev/null +++ b/apps/stats/package.json @@ -0,0 +1,90 @@ +{ + "name": "@tryghost/stats", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/TryGhost/Ghost/tree/main/apps/stats" + }, + "author": "Ghost Foundation", + "files": [ + "LICENSE", + "README.md", + "dist/" + ], + "main": "./dist/stats.umd.cjs", + "module": "./dist/stats.js", + "exports": { + ".": { + "import": "./dist/stats.js", + "require": "./dist/stats.umd.cjs" + }, + "./api": "./src/api.ts" + }, + "private": true, + "scripts": { + "dev": "vite build --watch", + "dev:start": "vite", + "test": "pnpm test:unit --coverage", + "test:unit": "vitest run test/unit", + "test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' playwright test", + "test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 pnpm test:acceptance --headed", + "test:acceptance:full": "ALL_BROWSERS=1 pnpm test:acceptance", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "build": "tsc && vite build", + "lint": "pnpm run lint:code && pnpm run lint:test", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src", + "lint:code:fix": "eslint --ext .js,.ts,.cjs,.tsx --cache --fix src", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test", + "preview": "vite preview" + }, + "devDependencies": { + "@faker-js/faker": "9.9.0", + "@playwright/test": "1.59.1", + "@tanstack/react-query": "4.36.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "14.3.1", + "@types/jest": "29.5.14", + "@types/react": "18.3.28", + "@types/react-svg-map": "2.1.4", + "@vitest/coverage-v8": "^1.6.1", + "@vitejs/plugin-react": "4.7.0", + "dotenv": "17.3.1", + "msw": "2.12.14", + "tailwindcss": "^4.2.2", + "vite": "5.4.21", + "vite-plugin-svgr": "4.5.0", + "vitest": "1.6.1" + }, + "dependencies": { + "@svg-maps/world": "1.0.1", + "@tryghost/admin-x-framework": "workspace:*", + "@tryghost/shade": "workspace:*", + "i18n-iso-countries": "7.14.0", + "moment": "2.24.0", + "moment-timezone": "0.5.45", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-svg-map": "2.2.0" + }, + "nx": { + "targets": { + "dev": { + "dependsOn": [ + "^build" + ] + }, + "test:unit": { + "dependsOn": [ + "^build" + ] + }, + "test:acceptance": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/apps/stats/playwright.config.mjs b/apps/stats/playwright.config.mjs new file mode 100644 index 0000000..8fa5955 --- /dev/null +++ b/apps/stats/playwright.config.mjs @@ -0,0 +1,3 @@ +import {adminXPlaywrightConfig} from '@tryghost/admin-x-framework/playwright'; + +export default adminXPlaywrightConfig(); diff --git a/apps/stats/src/api.ts b/apps/stats/src/api.ts new file mode 100644 index 0000000..469b956 --- /dev/null +++ b/apps/stats/src/api.ts @@ -0,0 +1,6 @@ +/** + * Public API for cross-package imports. + * Admin uses these exports instead of reaching into src/ directly. + */ +export {default as GlobalDataProvider} from './providers/global-data-provider'; +export {routes} from './routes'; diff --git a/apps/stats/src/app.tsx b/apps/stats/src/app.tsx new file mode 100644 index 0000000..7a76793 --- /dev/null +++ b/apps/stats/src/app.tsx @@ -0,0 +1,34 @@ +import GlobalDataProvider from './providers/global-data-provider'; +import StatsErrorBoundary from '@components/errors/stats-error-boundary'; +import {APP_ROUTE_PREFIX, routes} from '@src/routes'; +import {AppProvider, BaseAppProps, FrameworkProvider, Outlet, RouterProvider} from '@tryghost/admin-x-framework'; +import {ShadeApp} from '@tryghost/shade/app'; + +export {useAppContext} from '@tryghost/admin-x-framework'; + +const App: React.FC = ({framework, designSystem, appSettings}) => { + return ( + + + + + + + + + + + + + + ); +}; + +export default App; diff --git a/apps/stats/src/index.tsx b/apps/stats/src/index.tsx new file mode 100644 index 0000000..74d98a5 --- /dev/null +++ b/apps/stats/src/index.tsx @@ -0,0 +1,6 @@ +import './styles/index.css'; +import App from './app'; + +export { + App as AdminXApp +}; diff --git a/apps/stats/src/routes.tsx b/apps/stats/src/routes.tsx new file mode 100644 index 0000000..7f410b7 --- /dev/null +++ b/apps/stats/src/routes.tsx @@ -0,0 +1,27 @@ +import {RouteObject, lazyComponent} from '@tryghost/admin-x-framework'; + +export const APP_ROUTE_PREFIX = '/'; + +export const routes: RouteObject[] = [ + { + path: 'analytics', + children: [ + { + index: true, + lazy: lazyComponent(() => import('./views/Stats/Overview')) + }, + { + path: 'web', + lazy: lazyComponent(() => import('./views/Stats/Web')) + }, + { + path: 'growth', + lazy: lazyComponent(() => import('./views/Stats/Growth')) + }, + { + path: 'newsletters', + lazy: lazyComponent(() => import('./views/Stats/Newsletters')) + } + ] + } +]; diff --git a/apps/stats/src/standalone.tsx b/apps/stats/src/standalone.tsx new file mode 100644 index 0000000..69427f0 --- /dev/null +++ b/apps/stats/src/standalone.tsx @@ -0,0 +1,20 @@ +import './styles/index.css'; +import App from './app'; +import renderShadeApp from '@tryghost/admin-x-framework/test/render-shade'; +import {AppSettings} from '@tryghost/admin-x-framework'; + +// Use test overrides if available, otherwise use defaults +const defaultAppSettings: AppSettings = { + paidMembersEnabled: true, + newslettersEnabled: true, + analytics: { + emailTrackOpens: true, + emailTrackClicks: true, + membersTrackSources: true, + outboundLinkTagging: true, + webAnalytics: true + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +renderShadeApp(App, {appSettings: defaultAppSettings} as any); diff --git a/apps/stats/test/.eslintrc.cjs b/apps/stats/test/.eslintrc.cjs new file mode 100644 index 0000000..42f8e77 --- /dev/null +++ b/apps/stats/test/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/ts-test' + ] +}; diff --git a/apps/stats/test/setup.ts b/apps/stats/test/setup.ts new file mode 100644 index 0000000..e297e63 --- /dev/null +++ b/apps/stats/test/setup.ts @@ -0,0 +1,5 @@ +import '@testing-library/jest-dom/vitest'; +import {setupShadeMocks} from '@tryghost/admin-x-framework/test/setup'; + +// Set up common mocks for shade components +setupShadeMocks(); diff --git a/apps/stats/tsconfig.declaration.json b/apps/stats/tsconfig.declaration.json new file mode 100644 index 0000000..c7b87e9 --- /dev/null +++ b/apps/stats/tsconfig.declaration.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "composite": true, + "declaration": true, + "declarationMap": true, + "declarationDir": "./types", + "emitDeclarationOnly": true, + "tsBuildInfoFile": "./types/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["src/**/*.stories.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"] +} diff --git a/apps/stats/tsconfig.json b/apps/stats/tsconfig.json new file mode 100644 index 0000000..192ef77 --- /dev/null +++ b/apps/stats/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases */ + "baseUrl": "./src", + "paths": { + "@src/*": ["*"], + "@assets/*": ["assets/*"], + "@components/*": ["components/*"], + "@hooks/*": ["hooks/*"], + "@utils/*": ["utils/*"], + "@views/*": ["views/*"] + } + }, + "include": ["src", "test"] +} diff --git a/apps/stats/vite.config.mjs b/apps/stats/vite.config.mjs new file mode 100644 index 0000000..7af2941 --- /dev/null +++ b/apps/stats/vite.config.mjs @@ -0,0 +1,32 @@ +import adminXViteConfig from '@tryghost/admin-x-framework/vite'; +import pkg from './package.json'; +import svgr from 'vite-plugin-svgr'; +import {resolve} from 'path'; + +export default (function viteConfig() { + return adminXViteConfig({ + packageName: pkg.name, + plugins: [ + svgr() + ], + entry: resolve(__dirname, 'src/index.tsx'), + overrides: { + test: { + include: [ + './test/unit/**/*', + './src/**/*.test.ts' + ] + }, + resolve: { + alias: { + '@src': resolve(__dirname, './src'), + '@assets': resolve(__dirname, './src/assets'), + '@components': resolve(__dirname, './src/components'), + '@hooks': resolve(__dirname, './src/hooks'), + '@utils': resolve(__dirname, './src/utils'), + '@views': resolve(__dirname, './src/views') + } + } + } + }); +}); diff --git a/apps/stats/vitest.config.ts b/apps/stats/vitest.config.ts new file mode 100644 index 0000000..5e00474 --- /dev/null +++ b/apps/stats/vitest.config.ts @@ -0,0 +1,13 @@ +import {createVitestConfig} from '@tryghost/admin-x-framework/test/vitest-config'; +import {resolve} from 'path'; + +export default createVitestConfig({ + aliases: { + '@src': resolve(__dirname, './src'), + '@assets': resolve(__dirname, './src/assets'), + '@components': resolve(__dirname, './src/components'), + '@hooks': resolve(__dirname, './src/hooks'), + '@utils': resolve(__dirname, './src/utils'), + '@views': resolve(__dirname, './src/views') + } +}); diff --git a/compose.dev.analytics.yaml b/compose.dev.analytics.yaml new file mode 100644 index 0000000..31f10aa --- /dev/null +++ b/compose.dev.analytics.yaml @@ -0,0 +1,86 @@ +# Analytics (Tinybird) configuration for Ghost development environment +# Use with: docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml up +# +# This file adds Tinybird analytics services and configuration to ghost-dev. + +services: + analytics: + image: ghost/traffic-analytics:1.0.175@sha256:853fae2f7db4d280502caf4ac51090ad232395a667f405f22beb089c07ec7bcb + container_name: ghost-dev-analytics + platform: linux/amd64 + command: ["node", "--enable-source-maps", "dist/server.js"] + entrypoint: ["/app/entrypoint.sh"] + expose: + - "3000" + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000').then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))\""] + interval: 1s + retries: 120 + volumes: + - ./docker/analytics/entrypoint.sh:/app/entrypoint.sh:ro + - shared-config:/mnt/shared-config:ro + environment: + - PROXY_TARGET=http://tinybird-local:7181/v0/events + - TINYBIRD_WAIT=true + depends_on: + tinybird-local: + condition: service_healthy + tb-cli: + condition: service_completed_successfully + + tinybird-local: + image: tinybirdco/tinybird-local:latest@sha256:52ea15fc337547b13d06069c23479c293e23074d4e4a6be21253e4bd57ad12be + container_name: ghost-dev-tinybird + platform: linux/amd64 + stop_grace_period: 2s + ports: + - "7181:7181" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7181/v0/health"] + interval: 1s + timeout: 5s + retries: 120 + + tb-cli: + build: + context: ./ + dockerfile: docker/tb-cli/Dockerfile + container_name: ghost-dev-tb-cli + working_dir: /home/tinybird + environment: + - TB_HOST=http://tinybird-local:7181 + - TB_LOCAL_HOST=tinybird-local + volumes: + - ./ghost/core/core/server/data/tinybird:/home/tinybird + - shared-config:/mnt/shared-config + depends_on: + tinybird-local: + condition: service_healthy + + ghost-dev: + volumes: + # Mount shared-config volume to access Tinybird tokens created by tb-cli + - shared-config:/mnt/shared-config:ro + environment: + # Analytics configuration + analytics__url: http://analytics:3000 + analytics__enabled: "true" + # Tinybird configuration + # These static values are set here; workspaceId and adminToken are sourced from + # /mnt/shared-config/.env.tinybird by docker/ghost-dev/entrypoint.sh + TB_HOST: http://tinybird-local:7181 + TB_LOCAL_HOST: tinybird-local + tinybird__stats__endpoint: http://tinybird-local:7181 + tinybird__stats__endpointBrowser: http://localhost:7181 + tinybird__tracker__endpoint: http://localhost:2368/.ghost/analytics/api/v1/page_hit + tinybird__tracker__datasource: analytics_events + depends_on: + analytics: + condition: service_healthy + tb-cli: + condition: service_completed_successfully + +volumes: + shared-config: + + diff --git a/compose.dev.mailgun.yaml b/compose.dev.mailgun.yaml new file mode 100644 index 0000000..8454030 --- /dev/null +++ b/compose.dev.mailgun.yaml @@ -0,0 +1,10 @@ +services: + ghost-dev: + environment: + # Route Ghost transactional/test emails via Mailgun SMTP instead of Mailpit. + # Values come from the host environment/.env file. + mail__transport: SMTP + mail__options__host: smtp.mailgun.org + mail__options__port: 587 + mail__options__auth__user: ${MAILGUN_SMTP_USER} + mail__options__auth__pass: ${MAILGUN_SMTP_PASS} diff --git a/compose.dev.sqlite.yaml b/compose.dev.sqlite.yaml new file mode 100644 index 0000000..9e724c0 --- /dev/null +++ b/compose.dev.sqlite.yaml @@ -0,0 +1,16 @@ +services: + mysql: + # Prevents mysql container from starting up + profiles: + - mysql + + ghost-dev: + volumes: + # Override the named volume so the SQLite DB is accessible on the host. + - ./ghost/core/content/data:/home/ghost/ghost/core/content/data + environment: + database__client: sqlite3 + database__connection__filename: content/data/ghost-dev.db + depends_on: + mysql: + required: false diff --git a/compose.dev.storage.yaml b/compose.dev.storage.yaml new file mode 100644 index 0000000..68fca38 --- /dev/null +++ b/compose.dev.storage.yaml @@ -0,0 +1,66 @@ +# Object Storage configuration for Ghost development environment +# Use with: docker compose -f compose.dev.yaml -f compose.dev.storage.yaml up +# +# This file adds MinIO (S3-compatible storage) and configures ghost-dev to use it. +# Without this file, Ghost uses local filesystem storage (the default). + +services: + minio: + image: minio/minio:RELEASE.2024-12-13T22-19-12Z@sha256:149fdd73108553247ceee85fc65466f51034bd6e145d6e0c0e415167f5f1274f + container_name: ghost-dev-minio + command: server /data --console-address ':9001' + ports: + - "9000:9000" # S3 API + - "9001:9001" # Web console + environment: + - MINIO_ROOT_USER=minio-user + - MINIO_ROOT_PASSWORD=minio-pass + volumes: + - minio-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] + interval: 1s + retries: 120 + + minio-setup: + image: minio/mc@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727 + container_name: ghost-dev-minio-setup + entrypoint: ["/bin/sh", "/setup.sh"] + environment: + - MINIO_ROOT_USER=minio-user + - MINIO_ROOT_PASSWORD=minio-pass + - MINIO_BUCKET=ghost-dev + volumes: + - ./docker/minio/setup.sh:/setup.sh:ro + depends_on: + minio: + condition: service_healthy + restart: "no" + + ghost-dev: + environment: + # Object Storage - S3Storage adapter with MinIO backend + storage__media__adapter: S3Storage + storage__media__staticFileURLPrefix: content/media + storage__files__adapter: S3Storage + storage__files__staticFileURLPrefix: content/files + storage__S3Storage__bucket: ghost-dev + storage__S3Storage__region: us-east-1 + storage__S3Storage__tenantPrefix: ab/ab1234567890abcdef1234567890abcd + storage__S3Storage__forcePathStyle: "true" + storage__S3Storage__cdnUrl: http://127.0.0.1:9000/ghost-dev + storage__S3Storage__staticFileURLPrefix: content/images + storage__S3Storage__endpoint: http://minio:9000 + storage__S3Storage__accessKeyId: minio-user + storage__S3Storage__secretAccessKey: minio-pass + urls__media: http://127.0.0.1:9000/ghost-dev/ab/ab1234567890abcdef1234567890abcd + urls__files: http://127.0.0.1:9000/ghost-dev/ab/ab1234567890abcdef1234567890abcd + depends_on: + minio: + condition: service_healthy + minio-setup: + condition: service_completed_successfully + +volumes: + minio-data: + diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..f51b6e8 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,151 @@ +name: ghost-dev + +services: + mysql: + image: mysql:8.4.5@sha256:679e7e924f38a3cbb62a3d7df32924b83f7321a602d3f9f967c01b3df18495d6 + container_name: ghost-dev-mysql + command: --innodb-buffer-pool-size=1G --innodb-log-buffer-size=500M --innodb-change-buffer-max-size=50 --innodb-flush-log-at-trx_commit=0 --innodb-flush-method=O_DIRECT + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-ghost_dev} + MYSQL_USER: ghost + MYSQL_PASSWORD: ghost + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysql", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}", "-e", "SELECT 1"] + interval: 1s + retries: 120 + timeout: 5s + start_period: 10s + + redis: + image: redis:7.0@sha256:352c1fdadc91926edda08f45aeb3f27f37194c2f14101229c0523a11195c96e3 + container_name: ghost-dev-redis + ports: + - "6379:6379" + volumes: + - redis-data:/data + healthcheck: + test: + - CMD + - redis-cli + - --raw + - incr + - ping + interval: 1s + retries: 120 + + mailpit: + image: axllent/mailpit@sha256:0b5c5f7ffd3c93474baa7fd3869c1462e5a3d03256ed0933dfc0e7d81d794036 + container_name: ghost-dev-mailpit + ports: + - "1025:1025" # SMTP server + - "8025:8025" # Web interface + - "8026:8025" # Web interface (for e2e tests) + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"] + interval: 1s + retries: 30 + + # Main development Ghost instance + # Additional instances can be created programmatically via E2E GhostManager + ghost-dev: + build: + context: ./ + dockerfile: docker/ghost-dev/Dockerfile + container_name: ghost-dev + working_dir: /home/ghost/ghost/core + command: ["pnpm", "dev"] + volumes: + - ./ghost:/home/ghost/ghost + # Mount specific content subdirectories to preserve themes/adapters from source + - ghost-dev-data:/home/ghost/ghost/core/content/data + - ghost-dev-images:/home/ghost/ghost/core/content/images + - ghost-dev-media:/home/ghost/ghost/core/content/media + - ghost-dev-files:/home/ghost/ghost/core/content/files + - ghost-dev-logs:/home/ghost/ghost/core/content/logs + - shared-config:/mnt/shared-config:ro + environment: + NODE_ENV: development + NODE_TLS_REJECT_UNAUTHORIZED: "0" + DEBUG: ${DEBUG:-} + database__client: mysql2 + database__connection__host: mysql + database__connection__user: root + database__connection__password: ${MYSQL_ROOT_PASSWORD:-root} + database__connection__database: ${MYSQL_DATABASE:-ghost_dev} + server__host: 0.0.0.0 + server__port: 2368 + mail__transport: SMTP + mail__options__host: mailpit + mail__options__port: 1025 + # Redis cache (optional) + adapters__cache__Redis__host: redis + adapters__cache__Redis__port: 6379 + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + mailpit: + condition: service_healthy + stripe: + condition: service_healthy + required: false + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:2368',{redirect:'manual'}).then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))"] + timeout: 5s + retries: 10 + start_period: 5s + + # Caddy reverse proxy for the main dev instance + # Routes Ghost backend + proxies asset requests to host dev servers + ghost-dev-gateway: + build: + context: ./docker/dev-gateway + dockerfile: Dockerfile + container_name: ghost-dev-gateway + ports: + - "2368:80" + - "80:80" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + # Mount Caddyfile for live config changes without rebuilding + - ./docker/dev-gateway/Caddyfile:/etc/caddy/Caddyfile:ro + depends_on: + ghost-dev: + condition: service_healthy + + stripe: + image: stripe/stripe-cli:latest@sha256:aa500a8812f08613296e322ce2dfccd02459273432fcc2e3a0b4f68919fce438 + container_name: ghost-dev-stripe + entrypoint: ["/entrypoint.sh"] + profiles: ["stripe"] + volumes: + - ./docker/stripe/entrypoint.sh:/entrypoint.sh:ro + - shared-config:/mnt/shared-config + environment: + - GHOST_URL=${GHOST_URL:-http://ghost-dev:2368} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-} + healthcheck: + test: ["CMD", "test", "-f", "/mnt/shared-config/.env.stripe"] + interval: 1s + retries: 120 + +volumes: + mysql-data: + redis-data: + shared-config: + ghost-dev-data: + ghost-dev-images: + ghost-dev-media: + ghost-dev-files: + ghost-dev-logs: + +networks: + default: + name: ghost_dev diff --git a/docker/analytics/entrypoint.sh b/docker/analytics/entrypoint.sh new file mode 100755 index 0000000..0613bb0 --- /dev/null +++ b/docker/analytics/entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# Entrypoint script for the Analytics service in compose.yml +## This script configures the environment for the Analytics service to use Tinybird local. +## It depends on the `tb-cli` service, which creates the `.env` file, which is mounted +## into the Analytics service container at `/app/.env`. + +# Note: the analytics service's container is based on alpine, hence `sh` instead of `bash`. +set -eu + +# Initialize child process variable +child="" + +# Handle shutdown signals gracefully. +_term() { + echo "Caught SIGTERM/SIGINT signal, shutting down gracefully..." + if [ -n "$child" ]; then + kill -TERM "$child" 2>/dev/null || true + wait "$child" 2>/dev/null || true + fi + exit 0 +} + +# Set up signal handlers (POSIX-compliant signal names) +trap _term TERM INT + +# Set the TINYBIRD_TRACKER_TOKEN environment variable from the .env file +# This file is created by the `tb-cli` service and mounted into the Analytics service container +if [ -f /mnt/shared-config/.env.tinybird ]; then + . /mnt/shared-config/.env.tinybird + if [ -n "${TINYBIRD_TRACKER_TOKEN:-}" ]; then + export TINYBIRD_TRACKER_TOKEN="$TINYBIRD_TRACKER_TOKEN" + echo "Tinybird tracker token configured successfully" + else + echo "WARNING: TINYBIRD_TRACKER_TOKEN not found in /mnt/shared-config/.env.tinybird" >&2 + fi +else + echo "WARNING: /mnt/shared-config/.env.tinybird file not found - Tinybird tracking may not work" >&2 +fi + +# Start the process in the background +"$@" & +child=$! + +# Wait for the child process +wait "$child" diff --git a/docker/caddy/Caddyfile b/docker/caddy/Caddyfile new file mode 100644 index 0000000..24cd0df --- /dev/null +++ b/docker/caddy/Caddyfile @@ -0,0 +1,59 @@ +{ + local_certs +} + +# Run `sudo ./docker/caddy/trust_caddy_ca.sh` while the caddy container is running to trust the Caddy CA +(common_ghost_config) { + + log { + output stdout + format json + } + + # Proxy analytics requests with any prefix (e.g. /.ghost/analytics/ or /blog/.ghost/analytics/) + @analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$ + handle @analytics_paths { + rewrite * {re.analytics_match.2} + reverse_proxy {$ANALYTICS_PROXY_TARGET} + } + + handle /ember-cli-live-reload.js { + reverse_proxy admin:4200 + } + + reverse_proxy server:2368 +} + +# Allow http to be used +## Disables automatic redirect to https in development +http://localhost { + import common_ghost_config +} + +# Allow https to be used by explicitly requesting https://localhost +## Note: Caddy uses self-signed certificates. Your browser will warn you about this. +## Run `sudo ./docker/caddy/trust_caddy_ca.sh` while the caddy container is running to trust the Caddy CA +https://localhost { + import common_ghost_config +} + +# Access Ghost at https://site.ghost +## Add the following to your /etc/hosts file: +## 127.0.0.1 site.ghost +site.ghost { + reverse_proxy server:2368 +} + +# Access Ghost Admin at https://admin.ghost/ghost +## Add the following to your /etc/hosts file: +## 127.0.0.1 admin.ghost +admin.ghost { + handle /ember-cli-live-reload.js { + reverse_proxy admin:4200 + } + + handle { + reverse_proxy server:2368 + } +} + diff --git a/docker/caddy/Caddyfile.e2e b/docker/caddy/Caddyfile.e2e new file mode 100644 index 0000000..375b036 --- /dev/null +++ b/docker/caddy/Caddyfile.e2e @@ -0,0 +1,18 @@ +# E2E Test Caddyfile - Routes analytics requests to the analytics service +:80 { + log { + output stdout + format json + } + + # Proxy analytics requests with any prefix (e.g. /.ghost/analytics/ or /blog/.ghost/analytics/) + @analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$ + handle @analytics_paths { + rewrite * {re.analytics_match.2} + reverse_proxy {$ANALYTICS_PROXY_TARGET} + } + + # Default response for healthcheck and other requests + # E2E tests create Ghost instances dynamically, so we don't proxy to a fixed server + respond "OK" 200 +} \ No newline at end of file diff --git a/docker/caddy/trust_caddy_ca.sh b/docker/caddy/trust_caddy_ca.sh new file mode 100755 index 0000000..7de677f --- /dev/null +++ b/docker/caddy/trust_caddy_ca.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# --- Configuration --- +# !! IMPORTANT: Set this to your Caddy Docker container name or ID !! +CADDY_CONTAINER_NAME="ghost-caddy" # PLEASE UPDATE IF YOUR CONTAINER NAME IS DIFFERENT + +# Path where Caddy stores its local root CA inside the container +CADDY_INTERNAL_CERT_PATH="/data/caddy/pki/authorities/local/root.crt" + +# Temporary path on your host to save the certificate +HOST_TEMP_CERT_PATH="./caddy_local_root_for_keychain.crt" +# --- End Configuration --- + +# Check if running as root (needed for 'security add-trusted-cert' and /etc/hosts modification) +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root (e.g., using sudo) to modify the System Keychain." + exit 1 +fi + +echo "--- Managing Caddy Local CA Trust ---" +echo "Attempting to copy Caddy's local root CA certificate from container '$CADDY_CONTAINER_NAME'..." + +# Step 1: Copy the certificate from the Docker container +docker cp "${CADDY_CONTAINER_NAME}:${CADDY_INTERNAL_CERT_PATH}" "${HOST_TEMP_CERT_PATH}" +if [ $? -ne 0 ]; then + echo "Error: Failed to copy certificate from Docker container." + echo "Please ensure the container name '$CADDY_CONTAINER_NAME' is correct and the container is running." + echo "Also, Caddy needs to have served an HTTPS site at least once to generate its local CA." + exit 1 +fi +echo "Certificate copied successfully to ${HOST_TEMP_CERT_PATH}" + +echo "Adding certificate to System Keychain and trusting it..." + +# Step 2: Add the certificate to the System Keychain and set trust settings +security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" "${HOST_TEMP_CERT_PATH}" + +if [ $? -ne 0 ]; then + echo "Error: Failed to add or trust the certificate in Keychain." + echo "You might see a duplicate if a previous version of this CA with the same name was already added but not fully trusted." + # Clean up the temp cert + rm -f "${HOST_TEMP_CERT_PATH}" + exit 1 +fi + +echo "Certificate successfully added to System Keychain and trusted." + +# Step 3: Clean up the temporary certificate file +rm -f "${HOST_TEMP_CERT_PATH}" +echo "Temporary certificate file cleaned up." +echo "--- Caddy Local CA Trust complete ---" + +echo "" +echo "Script finished." +echo "IMPORTANT: You may need to restart your web browser(s) and/or clear your browser cache for the changes to take full effect." + +exit 0 \ No newline at end of file diff --git a/docker/dev-gateway/Caddyfile b/docker/dev-gateway/Caddyfile new file mode 100644 index 0000000..9a30215 --- /dev/null +++ b/docker/dev-gateway/Caddyfile @@ -0,0 +1,219 @@ +{ + admin off +} + +:80 { + # Compact log format for development + log { + output stdout + format transform "{common_log}" + } + + # Ember live reload (runs on separate port 4201) + # This handles both the script injection and WebSocket connections + handle /ember-cli-live-reload.js { + reverse_proxy {env.ADMIN_LIVE_RELOAD_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + # Enable WebSocket support for live reload + header_up Connection {>Connection} + header_up Upgrade {>Upgrade} + } + } + + # Ghost API - must go to Ghost backend, not admin dev server + handle /ghost/api/* { + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + + # Always tell Ghost requests are HTTPS to prevent redirects + header_up X-Forwarded-Proto https + } + } + + # Analytics API - proxy analytics requests to analytics service + # Handles paths like /.ghost/analytics/* or /blog/.ghost/analytics/* + @analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$ + handle @analytics_paths { + rewrite * {re.analytics_match.2} + reverse_proxy {env.ANALYTICS_PROXY_TARGET} { + header_up Host {host} + header_up X-Forwarded-Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + } + } + + # ActivityPub API - proxy activityPub requests to activityPub service (running in separate project) + # Requires activitypub containers to be running via the ActivityPub project's docker-compose + handle /.ghost/activitypub/* { + reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } + + # WebFinger - required for ActivityPub federation + handle /.well-known/webfinger { + reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } + + # NodeInfo - required for ActivityPub federation + handle /.well-known/nodeinfo { + reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } + + # Public app dev server assets - must come BEFORE general /ghost/* handler + # Ghost is configured to load these from /ghost/assets/* via compose.dev.yaml + handle /ghost/assets/* { + # Strip /ghost/assets/ prefix + uri strip_prefix /ghost/assets + + # Koenig Lexical Editor (optional - for developing Lexical in separate Koenig repo) + # Requires EDITOR_URL=/ghost/assets/koenig-lexical/ when starting admin dev server + # Falls back to Ghost backend (built package) via handle_errors if dev server isn't running + @lexical path /koenig-lexical/* + handle @lexical { + uri strip_prefix /koenig-lexical + reverse_proxy {env.LEXICAL_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + # Fail quickly if dev server is down + fail_duration 1s + unhealthy_request_count 1 + } + } + + # Portal + @portal path /portal/* + handle @portal { + uri strip_prefix /portal + reverse_proxy {env.PORTAL_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + + # Comments UI + @comments path /comments-ui/* + handle @comments { + uri strip_prefix /comments-ui + reverse_proxy {env.COMMENTS_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + + # Signup Form + @signup path /signup-form/* + handle @signup { + uri strip_prefix /signup-form + reverse_proxy {env.SIGNUP_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + + # Sodo Search + @search path /sodo-search/* + handle @search { + uri strip_prefix /sodo-search + reverse_proxy {env.SEARCH_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + + # Announcement Bar + @announcement path /announcement-bar/* + handle @announcement { + uri strip_prefix /announcement-bar + reverse_proxy {env.ANNOUNCEMENT_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + + # Everything else under /ghost/assets/* goes to admin dev server + handle { + # Re-add the prefix we stripped for admin dev server + rewrite * /ghost/assets{path} + reverse_proxy {env.ADMIN_DEV_SERVER} { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Forwarded-Host {host} + } + } + } + + # Auth frame - must go to Ghost backend for comment admin authentication + handle /ghost/auth-frame/* { + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } + + # JWKS endpoint - must go to Ghost backend for JWT verification + handle /ghost/.well-known/* { + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } + + # Admin interface - served from admin dev server + # This includes /ghost/, etc. (but /ghost/assets/* is handled above) + # Also handles WebSocket upgrade requests for HMR + handle /ghost* { + reverse_proxy {env.ADMIN_DEV_SERVER} { + header_up X-Forwarded-Host {host} + } + } + + # Everything else goes to Ghost backend + handle { + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + + # Always tell Ghost requests are HTTPS to prevent redirects + header_up X-Forwarded-Proto https + } + } + + # Handle errors + handle_errors { + # Fallback for Lexical when dev server is unavailable (502/503/504) + # Forwards to Ghost backend which serves the built koenig-lexical package + @lexical_fallback `{http.request.orig_uri.path}.startsWith("/ghost/assets/koenig-lexical/")` + handle @lexical_fallback { + rewrite * {http.request.orig_uri.path} + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Forwarded-Proto https + } + } + + # Default error response + respond "{err.status_code} {err.status_text}" + } +} diff --git a/docker/dev-gateway/Caddyfile.build b/docker/dev-gateway/Caddyfile.build new file mode 100644 index 0000000..7e736b7 --- /dev/null +++ b/docker/dev-gateway/Caddyfile.build @@ -0,0 +1,36 @@ +# Build mode Caddyfile +# Used for testing pre-built images (local or registry) + +{ + admin off +} + +:80 { + log { + output stdout + format console + } + + # Analytics API - proxy to analytics service + # Handles paths like /.ghost/analytics/* or /blog/.ghost/analytics/* + @analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$ + handle @analytics_paths { + rewrite * {re.analytics_match.2} + reverse_proxy {env.ANALYTICS_PROXY_TARGET} { + header_up Host {host} + header_up X-Forwarded-Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + } + } + + # Everything else to Ghost + handle { + reverse_proxy {env.GHOST_BACKEND} { + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto https + } + } +} diff --git a/docker/dev-gateway/Dockerfile b/docker/dev-gateway/Dockerfile new file mode 100644 index 0000000..4e40914 --- /dev/null +++ b/docker/dev-gateway/Dockerfile @@ -0,0 +1,19 @@ +FROM caddy:2-alpine@sha256:fce4f15aad23222c0ac78a1220adf63bae7b94355d5ea28eee53910624acedfa + +RUN caddy add-package github.com/caddyserver/transform-encoder + +# Default proxy targets (can be overridden via environment variables) +ENV GHOST_BACKEND=ghost-dev:2368 \ + ADMIN_DEV_SERVER=host.docker.internal:5174 \ + ADMIN_LIVE_RELOAD_SERVER=host.docker.internal:4200 \ + PORTAL_DEV_SERVER=host.docker.internal:4175 \ + COMMENTS_DEV_SERVER=host.docker.internal:7173 \ + SIGNUP_DEV_SERVER=host.docker.internal:6174 \ + SEARCH_DEV_SERVER=host.docker.internal:4178 \ + ANNOUNCEMENT_DEV_SERVER=host.docker.internal:4177 \ + LEXICAL_DEV_SERVER=host.docker.internal:4173 \ + ANALYTICS_PROXY_TARGET=analytics:3000 \ + ACTIVITYPUB_PROXY_TARGET=host.docker.internal:8080 + +COPY Caddyfile /etc/caddy/Caddyfile +EXPOSE 80 2368 diff --git a/docker/dev-gateway/README.md b/docker/dev-gateway/README.md new file mode 100644 index 0000000..4fcfc25 --- /dev/null +++ b/docker/dev-gateway/README.md @@ -0,0 +1,55 @@ +# Dev Gateway (Caddy) +This directory contains the Caddy reverse proxy configuration for the Ghost development environment. + +## Purpose +The Caddy reverse proxy container: +1. **Routes Ghost requests** to the Ghost container backend +2. **Proxies asset requests** to local dev servers running on the host +3. **Enables hot-reload** for frontend development without rebuilding Ghost + +## Configuration +### Environment Variables +Caddy uses environment variables (set in `compose.dev.yaml`) to configure proxy targets: + +- `GHOST_BACKEND` - Ghost container hostname (e.g., `ghost-dev:2368`) +- `ADMIN_DEV_SERVER` - React admin dev server (e.g., `host.docker.internal:5174`) +- `ADMIN_LIVE_RELOAD_SERVER` - Ember live reload WebSocket (e.g., `host.docker.internal:4200`) +- `PORTAL_DEV_SERVER` - Portal dev server (e.g., `host.docker.internal:4175`) +- `COMMENTS_DEV_SERVER` - Comments UI (e.g., `host.docker.internal:7173`) +- `SIGNUP_DEV_SERVER` - Signup form (e.g., `host.docker.internal:6174`) +- `SEARCH_DEV_SERVER` - Sodo search (e.g., `host.docker.internal:4178`) +- `ANNOUNCEMENT_DEV_SERVER` - Announcement bar (e.g., `host.docker.internal:4177`) +- `LEXICAL_DEV_SERVER` - *Optional:* Local Koenig Lexical editor dev server (e.g., `host.docker.internal:4173`) + - For developing Lexical in the separate [Koenig repository](https://github.com/TryGhost/Koenig) + - Requires `EDITOR_URL=/ghost/assets/koenig-lexical/` when starting admin dev server + - Automatically falls back to Ghost backend (built package) if dev server is not running +- `ACTIVITYPUB_PROXY_TARGET` - *Optional:* ActivityPub service (e.g., `host.docker.internal:8080`) + - For developing with the [ActivityPub project](https://github.com/TryGhost/ActivityPub) running locally + - Requires the ActivityPub docker-compose services to be running + +**Note:** AdminX React apps (admin-x-settings, activitypub, posts, stats) are served through the admin dev server so they don't need separate proxy entries. + +### Ghost Configuration +Ghost is configured via environment variables in `compose.dev.yaml` to load public app assets from `/ghost/assets/*` (e.g., `portal__url: /ghost/assets/portal/portal.min.js`). This uses the same path structure as built admin assets. + +### Routing Rules +The Caddyfile defines these routing rules: + +| Path Pattern | Target | Purpose | +|--------------------------------------|-------------------------------------|------------------------------------------------------------------------| +| `/ember-cli-live-reload.js` | Admin live reload (port 4200) | Ember hot-reload script and WebSocket | +| `/ghost/api/*` | Ghost backend | Ghost API (bypasses admin dev server) | +| `/.ghost/activitypub/*` | ActivityPub server (port 8080) | *Optional:* ActivityPub API (requires AP project running) | +| `/.well-known/webfinger` | ActivityPub server (port 8080) | *Optional:* WebFinger for federation | +| `/.well-known/nodeinfo` | ActivityPub server (port 8080) | *Optional:* NodeInfo for federation | +| `/ghost/assets/koenig-lexical/*` | Lexical dev server (port 4173) | *Optional:* Koenig Lexical editor (falls back to Ghost if not running) | +| `/ghost/assets/portal/*` | Portal dev server (port 4175) | Membership UI | +| `/ghost/assets/comments-ui/*` | Comments dev server (port 7173) | Comments widget | +| `/ghost/assets/signup-form/*` | Signup dev server (port 6174) | Signup form widget | +| `/ghost/assets/sodo-search/*` | Search dev server (port 4178) | Search widget (JS + CSS) | +| `/ghost/assets/announcement-bar/*` | Announcement dev server (port 4177) | Announcement widget | +| `/ghost/assets/*` | Admin dev server (port 5174) | Other admin assets (fallback) | +| `/ghost/*` | Admin dev server (port 5174) | Admin interface | +| Everything else | Ghost backend | Main Ghost application | + +**Note:** All port numbers listed are the host ports where dev servers run by default. diff --git a/docker/development.entrypoint.sh b/docker/development.entrypoint.sh new file mode 100755 index 0000000..ceb860e --- /dev/null +++ b/docker/development.entrypoint.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -euo pipefail + +# Runs `pnpm install` if `pnpm-lock.yaml` has changed to avoid a full `docker build` when changing branches/dependencies +## Dockerfile calculates a hash and stores it in `.pnpmhash/pnpm-lock.yaml.md5` +## compose.yml mounts a named volume to persist the `.pnpmhash` directory +( + cd /home/ghost + pnpm_lock_hash_file_path=".pnpmhash/pnpm-lock.yaml.md5" + calculated_hash=$(md5sum pnpm-lock.yaml | awk '{print $1}') + + if [ -f "$pnpm_lock_hash_file_path" ]; then + stored_hash=$(cat "$pnpm_lock_hash_file_path") + if [ "$calculated_hash" != "$stored_hash" ]; then + echo "INFO: pnpm-lock.yaml has changed. Running pnpm install..." + pnpm install + mkdir -p .pnpmhash + echo "$calculated_hash" > "$pnpm_lock_hash_file_path" + fi + else + echo "WARNING: pnpm-lock.yaml hash file ($pnpm_lock_hash_file_path) not found. Running pnpm install as a precaution." + pnpm install + mkdir -p .pnpmhash + echo "$calculated_hash" > "$pnpm_lock_hash_file_path" + fi +) + +# Configure Ghost to use Tinybird Local +if [ -f /mnt/shared-config/.env.tinybird ]; then + source /mnt/shared-config/.env.tinybird + if [ -n "${TINYBIRD_WORKSPACE_ID:-}" ] && [ -n "${TINYBIRD_ADMIN_TOKEN:-}" ]; then + export tinybird__workspaceId="$TINYBIRD_WORKSPACE_ID" + export tinybird__adminToken="$TINYBIRD_ADMIN_TOKEN" + else + echo "WARNING: Tinybird not enabled: Missing required environment variables" + fi +else + echo "WARNING: Tinybird not enabled: .env file not found" +fi + +# Configure Stripe webhook secret +if [ -f /mnt/shared-config/.env.stripe ]; then + source /mnt/shared-config/.env.stripe + if [ -n "${STRIPE_WEBHOOK_SECRET:-}" ]; then + export WEBHOOK_SECRET="$STRIPE_WEBHOOK_SECRET" + echo "Stripe webhook secret configured successfully" + else + echo "WARNING: Stripe webhook secret not found in shared config" + fi +fi + +pnpm nx reset + +# Execute the CMD +exec "$@" diff --git a/docker/ghost-dev/Dockerfile b/docker/ghost-dev/Dockerfile new file mode 100644 index 0000000..3e03736 --- /dev/null +++ b/docker/ghost-dev/Dockerfile @@ -0,0 +1,58 @@ +# Minimal Development Dockerfile for Ghost Core +# Source code is mounted at runtime for hot-reload support + +ARG NODE_VERSION=22.18.0 + +FROM node:$NODE_VERSION-bullseye-slim + +# Install system dependencies needed for building native modules +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + curl \ + python3 \ + git && \ + rm -rf /var/lib/apt/lists/* && \ + apt clean + +WORKDIR /home/ghost + +# Copy package files for dependency installation +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ +COPY ghost/core/package.json ghost/core/package.json +COPY ghost/i18n/package.json ghost/i18n/package.json +COPY ghost/parse-email-address/package.json ghost/parse-email-address/package.json + +# Install dependencies +# Note: Dependencies are installed at build time, but source code is mounted at runtime. + +# Copy root lifecycle scripts/hooks needed by `pnpm install` +COPY .github/scripts .github/scripts +COPY .github/hooks .github/hooks + +# Enable corepack so it can read packageManager from package.json and provide pnpm +RUN corepack enable + +# Install deps with a persistent pnpm store cache to speed up rebuilds +RUN --mount=type=cache,target=/root/.local/share/pnpm/store,id=pnpm-store \ + pnpm install --frozen-lockfile --prefer-offline + +# Copy entrypoint script that optionally loads Tinybird config +COPY docker/ghost-dev/entrypoint.sh entrypoint.sh +RUN chmod +x entrypoint.sh + +# Public app assets are served via /ghost/assets/* in dev mode. +# Caddy forwards these paths to host frontend dev servers. +ENV portal__url=/ghost/assets/portal/portal.min.js \ + comments__url=/ghost/assets/comments-ui/comments-ui.min.js \ + sodoSearch__url=/ghost/assets/sodo-search/sodo-search.min.js \ + sodoSearch__styles=/ghost/assets/sodo-search/main.css \ + signupForm__url=/ghost/assets/signup-form/signup-form.min.js \ + announcementBar__url=/ghost/assets/announcement-bar/announcement-bar.min.js + +# Source code will be mounted from host at /home/ghost/ghost/core +# This allows the Ghost dev script to pick up file changes for hot-reload +WORKDIR /home/ghost/ghost/core + +ENTRYPOINT ["/home/ghost/entrypoint.sh"] +CMD ["pnpm", "dev"] diff --git a/docker/ghost-dev/README.md b/docker/ghost-dev/README.md new file mode 100644 index 0000000..71e13a2 --- /dev/null +++ b/docker/ghost-dev/README.md @@ -0,0 +1,35 @@ +# Ghost Core Dev Docker Image + +Minimal Docker image for running Ghost Core in development with hot-reload support. + +## Purpose + +This lightweight image: +- Installs only Ghost Core dependencies +- Mounts source code from the host at runtime +- Enables `nodemon` for automatic restarts on file changes +- Works with the Caddy gateway to proxy frontend assets from host dev servers + +## Key Differences from Main Dockerfile + +**Main `Dockerfile`** (for E2E tests, full builds): +- Builds all frontend apps (Admin, Portal, AdminX apps, etc.) +- Bundles everything into the image +- ~15 build stages, 5-10 minute build time + +**This `Dockerfile`** (for local development): +- Only installs dependencies +- No frontend builds or bundling +- Source code mounted at runtime +- Used for: Local development with `pnpm dev` + +## Usage + +This image is used automatically when running: + +```bash +pnpm dev # Starts Docker backend + frontend dev servers on host +pnpm dev:analytics # Include Tinybird analytics +pnpm dev:storage # Include MinIO S3-compatible object storage +pnpm dev:all # Include all optional services +``` diff --git a/docker/ghost-dev/entrypoint.sh b/docker/ghost-dev/entrypoint.sh new file mode 100755 index 0000000..5cb1b33 --- /dev/null +++ b/docker/ghost-dev/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +# Configure Ghost to use Tinybird Local +# Sources tokens from /mnt/shared-config/.env.tinybird created by tb-cli +if [ -f /mnt/shared-config/.env.tinybird ]; then + source /mnt/shared-config/.env.tinybird + if [ -n "${TINYBIRD_WORKSPACE_ID:-}" ] && [ -n "${TINYBIRD_ADMIN_TOKEN:-}" ]; then + export tinybird__workspaceId="$TINYBIRD_WORKSPACE_ID" + export tinybird__adminToken="$TINYBIRD_ADMIN_TOKEN" + echo "Tinybird configuration loaded successfully" + else + echo "WARNING: Tinybird not enabled: Missing required environment variables in .env.tinybird" >&2 + fi +else + echo "WARNING: Tinybird not enabled: .env.tinybird file not found at /mnt/shared-config/.env.tinybird" >&2 +fi + + +# Configure Stripe webhook secret +if [ -f /mnt/shared-config/.env.stripe ]; then + source /mnt/shared-config/.env.stripe + if [ -n "${STRIPE_WEBHOOK_SECRET:-}" ]; then + export WEBHOOK_SECRET="$STRIPE_WEBHOOK_SECRET" + echo "Stripe webhook secret configured successfully" + else + echo "WARNING: Stripe webhook secret not found in shared config" + fi +fi + +# Execute the CMD +exec "$@" + diff --git a/docker/grafana/dashboard.yml b/docker/grafana/dashboard.yml new file mode 100644 index 0000000..bb0b691 --- /dev/null +++ b/docker/grafana/dashboard.yml @@ -0,0 +1,15 @@ +## This file is used to point to the folder where the dashboards are stored +## To edit or create a dashboard, add a .json file to the ./dashboards folder + +apiVersion: 1 + +providers: + - name: "Dashboard provider" + orgId: 1 + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true \ No newline at end of file diff --git a/docker/grafana/dashboards/main-dashboard.json b/docker/grafana/dashboards/main-dashboard.json new file mode 100644 index 0000000..4240128 --- /dev/null +++ b/docker/grafana/dashboards/main-dashboard.json @@ -0,0 +1,2242 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": "$datasource", + "enable": true, + "expr": "ghost_process_start_time_seconds{job=~\"$job\", instance=~\"$instance\"} * 1000", + "hide": false, + "iconColor": "#B877D9", + "name": "Ghost Start", + "showIn": 0, + "textFormat": "{{instance}}", + "titleFormat": "Ghost Start", + "useValueForTime": true + } + ] + }, + "description": "An overview of the Ghost process metrics.", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 14058, + "graphTooltip": 0, + "id": 1, + "iteration": 1731634781920, + "links": [], + "liveNow": false, + "panels": [ + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "The version of Node.js.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 2, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "first" + ], + "fields": "/^version$/", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_version_info{}", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "title": "Node.js Version", + "type": "stat" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "The number of times Node.js restarted.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 3, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "sum(changes(ghost_process_start_time_seconds{job=~\"$job\"}[$__range]))", + "instant": false, + "interval": "", + "legendFormat": "Node.js", + "refId": "A" + } + ], + "title": "Node.js Restarts", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 4, + "panels": [], + "title": "Process", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "CPU usage.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 4 + }, + "id": 5, + "interval": "1", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "expr": "rate(ghost_process_cpu_user_seconds_total{job=~\"$job\"}[1m]) * 100", + "interval": "", + "legendFormat": "User CPU - {{job}}", + "refId": "A" + }, + { + "expr": "rate(ghost_process_cpu_system_seconds_total{job=~\"$job\"}[1m]) * 100", + "interval": "", + "legendFormat": "System CPU - {{job}}", + "refId": "B" + }, + { + "expr": "rate(ghost_process_cpu_seconds_total{job=~\"$job\"}[1m]) * 100", + "interval": "", + "legendFormat": "Total CPU - {{job}}", + "refId": "C" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "CPU time spent.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 4 + }, + "id": 6, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "rate(ghost_process_cpu_seconds_total{job=~\"$job\"}[$interval])", + "interval": "", + "legendFormat": "Total - {{job}}", + "refId": "A" + }, + { + "exemplar": true, + "expr": "rate(ghost_process_cpu_system_seconds_total{job=~\"$job\"}[$interval])", + "hide": false, + "interval": "", + "legendFormat": "System - {{job}}", + "refId": "B" + }, + { + "exemplar": true, + "expr": "rate(ghost_process_cpu_user_seconds_total{job=~\"$job\"}[$interval])", + "hide": false, + "interval": "", + "legendFormat": "User - {{job}}", + "refId": "C" + } + ], + "title": "CPU Time Spent", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Memory usage.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 4 + }, + "id": 7, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "process_resident_memory_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Process Memory - {{job}}", + "refId": "A" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_heap_size_total_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Heap Total - {{job}}", + "refId": "B" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_heap_size_used_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Heap Used - {{job}}", + "refId": "C" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_external_memory_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "External Memory - {{job}}", + "refId": "D" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Number of active handle and active requests.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 13 + }, + "id": 8, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_active_handles_total{job=~\"$job\"}", + "interval": "", + "legendFormat": "Active Handler - {{job}}", + "refId": "A" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_active_requests_total{job=~\"$job\"}", + "interval": "", + "legendFormat": "Active Request - {{job}}", + "refId": "B" + } + ], + "title": "Active Handlers and Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Latency of the event loop.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 13 + }, + "id": 9, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_eventloop_lag_seconds{job=~\"$job\"}", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "Last - {{job}}", + "refId": "A" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_eventloop_lag_p99_seconds{job=~\"$job\"}", + "interval": "", + "legendFormat": "P99 - {{job}}", + "refId": "B" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_eventloop_lag_p50_seconds{job=~\"$job\"}", + "interval": "", + "legendFormat": "P50 - {{job}}", + "refId": "C" + } + ], + "title": "Event Loop Latency", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 18, + "panels": [], + "title": "Database: Connection Pool", + "type": "row" + }, + { + "description": "The number of active connections maintained with the database. These connections can be in use or idle.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 20, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_max{job=\"$job\"}", + "interval": "", + "legendFormat": "Max - {{job}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_active{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "Active - {{job}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_min{job=\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "Min - {{job}}", + "refId": "C" + } + ], + "title": "Connections", + "type": "timeseries" + }, + { + "description": "The number of active connections as a percentage of the maximum allowed by the pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 25, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_active{job=~\"$job\"} / ghost_db_connection_pool_max{job=~\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "Active - {{job}}", + "refId": "B" + } + ], + "title": "Pool Utilization", + "type": "timeseries" + }, + { + "description": "The elapsed time a transaction spends waiting for an available connection in the connection pool.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurations" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 32, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_acquire_duration_seconds{quantile=\"0.5\", job=~\"$job\"}", + "interval": "", + "legendFormat": "P50 - {{job}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_acquire_duration_seconds{quantile=\"0.9\", job=~\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "P90 - {{job}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_acquire_duration_seconds{quantile=\"0.99\", job=~\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "P99 - {{job}}", + "refId": "C" + } + ], + "title": "Time to Acquire Connection", + "type": "timeseries" + }, + { + "description": "The number of transactions that are currently in the queue waiting to acquire a free connection", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 31 + }, + "id": 24, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_pending_acquires{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "title": "Pending Acquires", + "type": "timeseries" + }, + { + "description": "The number of connections waiting to be established with the database.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 31 + }, + "id": 22, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_connection_pool_pending_creates{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "title": "Pending Creates", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 27, + "panels": [], + "title": "Database: Queries", + "type": "row" + }, + { + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurations" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 40 + }, + "id": 31, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_query_duration_seconds{quantile=\"0.5\", job=~\"$job\"}", + "interval": "", + "legendFormat": "P50 - {{job}}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_query_duration_seconds{quantile=\"0.9\", job=~\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "P90 - {{job}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "ghost_db_query_duration_seconds{quantile=\"0.99\", job=~\"$job\"}", + "hide": false, + "interval": "", + "legendFormat": "P99 - {{job}}", + "refId": "C" + } + ], + "title": "Query Duration", + "type": "timeseries" + }, + { + "description": "The number of queries executed in the trailing 60s.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "QPM" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 40 + }, + "id": 29, + "interval": "1s", + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "rate(ghost_db_query_duration_seconds_count{job=~\"$job\"}[1m])", + "interval": "", + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "title": "Query Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 48 + }, + "id": 10, + "panels": [], + "title": "Garbage Collector", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Rate of garbage collection duration.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 49 + }, + "id": 11, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "rate(ghost_nodejs_gc_duration_seconds_sum{job=~\"$job\"}[$interval])", + "interval": "", + "legendFormat": "{{kind}} - {{job}}", + "refId": "A" + } + ], + "title": "GC Duration Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Duration of garbage collection.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 49 + }, + "id": 12, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_gc_duration_seconds_sum{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{kind}} - {{job}}", + "refId": "A" + } + ], + "title": "GC Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Usage of heap memory.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 49 + }, + "id": 13, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_heap_size_total_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Total - {{job}}", + "refId": "A" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_heap_size_used_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Used - {{job}}", + "refId": "B" + }, + { + "exemplar": true, + "expr": "process_resident_memory_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "Resident - {{job}}", + "refId": "C" + }, + { + "exemplar": true, + "expr": "ghost_nodejs_external_memory_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "External - {{job}}", + "refId": "D" + } + ], + "title": "Heap Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Rate of garbage collection.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 57 + }, + "id": 14, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "rate(ghost_nodejs_gc_duration_seconds_count{job=~\"$job\"}[$interval])", + "interval": "", + "legendFormat": "{{kind}} - {{job}}", + "refId": "A" + } + ], + "title": "GC Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Count of garbage collection.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 2, + "type": "log" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 57 + }, + "id": 15, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_gc_duration_seconds_count{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{kind}} - {{job}}", + "refId": "A" + } + ], + "title": "GC Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Usage of heap space.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 57 + }, + "id": 16, + "interval": "1s", + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.3.0", + "targets": [ + { + "exemplar": true, + "expr": "ghost_nodejs_heap_space_size_used_bytes{job=~\"$job\"}", + "interval": "", + "legendFormat": "{{space}} - {{job}}", + "refId": "A" + } + ], + "title": "Heap Space Used", + "type": "timeseries" + } + ], + "refresh": "5s", + "schemaVersion": 33, + "style": "dark", + "tags": [ + "node.js", + "nodejs" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": true, + "text": [ + "ghost-chris-local" + ], + "value": [ + "ghost-chris-local" + ] + }, + "datasource": { + "uid": "$datasource" + }, + "definition": "label_values(ghost_nodejs_eventloop_lag_seconds, job)", + "hide": 0, + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": { + "query": "label_values(ghost_nodejs_eventloop_lag_seconds, job)", + "refId": "Prometheus-job-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "uid": "$datasource" + }, + "definition": "label_values(ghost_nodejs_eventloop_lag_seconds, instance)", + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(ghost_nodejs_eventloop_lag_seconds, instance)", + "refId": "Prometheus-instance-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "1m", + "value": "1m" + }, + "hide": 0, + "label": "Interval", + "name": "interval", + "options": [ + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Ghost Dashboard", + "uid": "yX2d7k1Gk", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/docker/grafana/datasources/datasource.yml b/docker/grafana/datasources/datasource.yml new file mode 100644 index 0000000..c6d2139 --- /dev/null +++ b/docker/grafana/datasources/datasource.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: +- name: Prometheus + type: prometheus + url: http://prometheus:9090 + isDefault: true + access: proxy + editable: true \ No newline at end of file diff --git a/docker/minio/setup.sh b/docker/minio/setup.sh new file mode 100644 index 0000000..92b8b6b --- /dev/null +++ b/docker/minio/setup.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -euo pipefail + +BUCKET=${MINIO_BUCKET:-ghost-dev} + +echo "Configuring MinIO alias..." +mc alias set local http://minio:9000 "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" + +echo "Ensuring bucket '${BUCKET}' exists..." +mc mb --ignore-existing "local/${BUCKET}" + +echo "Setting anonymous download policy on '${BUCKET}'..." +mc anonymous set download "local/${BUCKET}" + +echo "MinIO bucket '${BUCKET}' ready." diff --git a/docker/mysql-preload/.keep b/docker/mysql-preload/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..0645eae --- /dev/null +++ b/docker/prometheus/prometheus.yml @@ -0,0 +1,26 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'pushgateway' + + scrape_interval: 1s + + static_configs: + - targets: ['pushgateway:9091'] + + honor_labels: true + +remote_write: + - url: http://grafana:3000/api/prom/push \ No newline at end of file diff --git a/docker/stripe/entrypoint.sh b/docker/stripe/entrypoint.sh new file mode 100755 index 0000000..1055fc4 --- /dev/null +++ b/docker/stripe/entrypoint.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +# Entrypoint script for the Stripe CLI service in compose.yml +## This script fetches the webhook secret from Stripe CLI and writes it to a shared config file +## that the Ghost server can read to verify webhook signatures. + +# Note: the stripe CLI container is based on alpine, hence `sh` instead of `bash`. +set -eu + +# Initialize child process variable +child="" + +# Handle shutdown signals gracefully. +_term() { + echo "Caught SIGTERM/SIGINT signal, shutting down gracefully..." + if [ -n "$child" ]; then + kill -TERM "$child" 2>/dev/null || true + wait "$child" 2>/dev/null || true + fi + exit 0 +} + +# Set up signal handlers (POSIX-compliant signal names) +trap _term TERM INT + +# Remove any stale config file from previous runs +rm -f /mnt/shared-config/.env.stripe + +# Check if STRIPE_SECRET_KEY is set +if [ -z "${STRIPE_SECRET_KEY:-}" ]; then + echo "================================================================================" + echo "ERROR: STRIPE_SECRET_KEY is not set" + echo "" + echo "To use the Stripe service, you must set STRIPE_SECRET_KEY in your .env file:" + echo " STRIPE_SECRET_KEY=sk_test_..." + echo "" + echo "You can find your secret key at: https://dashboard.stripe.com/test/apikeys" + echo "================================================================================" + exit 1 +fi + +echo "Using STRIPE_SECRET_KEY for authentication" + +# Fetch the webhook secret with timeout +echo "Fetching Stripe webhook secret..." +WEBHOOK_SECRET=$(timeout 10s stripe listen --print-secret --api-key "${STRIPE_SECRET_KEY}" 2>&1 || echo "TIMEOUT") + +# Check if we got a timeout +if [ "$WEBHOOK_SECRET" = "TIMEOUT" ]; then + echo "ERROR: Timed out waiting for Stripe CLI (10s)" + echo "Please check that your STRIPE_SECRET_KEY is valid" + exit 1 +fi + +# Check if we got a valid secret (should start with "whsec_") +if echo "$WEBHOOK_SECRET" | grep -q "^whsec_"; then + echo "Successfully fetched webhook secret" +else + echo "ERROR: Failed to fetch Stripe webhook secret" + echo "Output: $WEBHOOK_SECRET" + echo "Please ensure STRIPE_SECRET_KEY is set in your environment" + exit 1 +fi + +# Write the webhook secret to the shared config file +ENV_FILE="/mnt/shared-config/.env.stripe" +TMP_ENV_FILE="/mnt/shared-config/.env.stripe.tmp" + +echo "Writing Stripe configuration to $ENV_FILE..." + +cat > "$TMP_ENV_FILE" << EOF +STRIPE_WEBHOOK_SECRET=$WEBHOOK_SECRET +EOF + +if [ $? -eq 0 ]; then + mv "$TMP_ENV_FILE" "$ENV_FILE" + if [ $? -eq 0 ]; then + echo "Successfully wrote Stripe configuration to $ENV_FILE" + else + echo "ERROR: Failed to move temporary file to $ENV_FILE" + exit 1 + fi +else + echo "ERROR: Failed to create temporary configuration file" + rm -f "$TMP_ENV_FILE" + exit 1 +fi + +# Start stripe listen in the background +echo "Starting Stripe webhook listener forwarding to ${GHOST_URL}/members/webhooks/stripe/" +stripe listen --forward-to ${GHOST_URL}/members/webhooks/stripe/ --api-key "${STRIPE_SECRET_KEY}" & +child=$! + +# Wait for the child process +wait "$child" diff --git a/docker/stripe/with-stripe.sh b/docker/stripe/with-stripe.sh new file mode 100755 index 0000000..7914511 --- /dev/null +++ b/docker/stripe/with-stripe.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Wrapper script to run commands with the Stripe profile enabled +# Checks for STRIPE_SECRET_KEY before starting, failing early with helpful error +# +# Usage: ./docker/stripe/with-stripe.sh +# Example: ./docker/stripe/with-stripe.sh nx run ghost-monorepo:docker:dev + +set -e + +check_stripe_key() { + # Check environment variable first + if [ -n "$STRIPE_SECRET_KEY" ]; then + return 0 + fi + + # Check .env file for non-empty value + if [ -f .env ] && grep -qE '^STRIPE_SECRET_KEY=.+' .env; then + return 0 + fi + + return 1 +} + +if ! check_stripe_key; then + echo "" + echo "================================================================================" + echo "ERROR: STRIPE_SECRET_KEY is not set" + echo "" + echo "To use the Stripe service, set STRIPE_SECRET_KEY in your .env file or ENV vars:" + echo " STRIPE_SECRET_KEY=sk_test_..." + echo "" + echo "You can find your secret key at: https://dashboard.stripe.com/test/apikeys" + echo "================================================================================" + echo "" + exit 1 +fi + +# Run the command with the stripe profile enabled +export COMPOSE_PROFILES="${COMPOSE_PROFILES:+$COMPOSE_PROFILES,}stripe" +exec "$@" diff --git a/docker/tb-cli/Dockerfile b/docker/tb-cli/Dockerfile new file mode 100644 index 0000000..a265241 --- /dev/null +++ b/docker/tb-cli/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.13-slim@sha256:eefe082c4b73082d83b8e7705ed999bc8a1dae57fe1ea723f907a0fc4b90f088 + +# Install uv from Astral.sh +COPY --from=ghcr.io/astral-sh/uv:0.11.6@sha256:b1e699368d24c57cda93c338a57a8c5a119009ba809305cc8e86986d4a006754 /uv /uvx /bin/ + +# Install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + jq \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /home/tinybird + +RUN uv tool install tinybird@0.0.1.dev285 --python 3.13 --force + +ENV PATH="/root/.local/bin:$PATH" + +COPY docker/tb-cli/entrypoint.sh /usr/local/bin +RUN chmod +x /usr/local/bin/entrypoint.sh +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/tb-cli/entrypoint.sh b/docker/tb-cli/entrypoint.sh new file mode 100755 index 0000000..61d3f4e --- /dev/null +++ b/docker/tb-cli/entrypoint.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Entrypoint script for the Tinybird CLI service in compose.yml +## This script deploys the Tinybird files to Tinybird local, then retrieves important configuration values +## and writes them to a .env file in /ghost/core/core/server/data/tinybird. This .env file is used by +## Ghost and the Analytics service to automatically configure their connections to Tinybird Local + +set -euo pipefail + +# Build the Tinybird files +tb --local build + +# Get the Tinybird workspace ID and admin token from the Tinybird Local container +TB_INFO=$(tb --output json info) + +# Get the workspace ID from the JSON output +WORKSPACE_ID=$(echo "$TB_INFO" | jq -r '.local.workspace_id') + +# Check if workspace ID is valid +if [ -z "$WORKSPACE_ID" ] || [ "$WORKSPACE_ID" = "null" ]; then + echo "Error: Failed to get workspace ID from Tinybird. Please ensure Tinybird is running and initialized." >&2 + exit 1 +fi + +WORKSPACE_TOKEN=$(echo "$TB_INFO" | jq -r '.local.token') + + +# Check if workspace token is valid +if [ -z "$WORKSPACE_TOKEN" ] || [ "$WORKSPACE_TOKEN" = "null" ]; then + echo "Error: Failed to get workspace token from Tinybird. Please ensure Tinybird is running and initialized." >&2 + exit 1 +fi +# +# Get the admin token from the Tinybird API +## This is different from the workspace admin token +echo "Fetching tokens from Tinybird API..." +MAX_RETRIES=10 +RETRY_DELAY=1 + +for i in $(seq 1 $MAX_RETRIES); do + set +e + TOKENS_RESPONSE=$(curl --fail --show-error -s -H "Authorization: Bearer $WORKSPACE_TOKEN" http://tinybird-local:7181/v0/tokens 2>&1) + CURL_EXIT=$? + set -e + + if [ $CURL_EXIT -eq 0 ]; then + # Find admin token by looking for ADMIN scope (more robust than name matching) + ADMIN_TOKEN=$(echo "$TOKENS_RESPONSE" | jq -r '.tokens[] | select(.scopes[]? | .type == "ADMIN") | .token' | head -n1) + + if [ -n "$ADMIN_TOKEN" ] && [ "$ADMIN_TOKEN" != "null" ]; then + break + fi + fi + + if [ $i -lt $MAX_RETRIES ]; then + echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..." >&2 + sleep $RETRY_DELAY + fi +done + +# Check if admin token is valid +if [ -z "$ADMIN_TOKEN" ] || [ "$ADMIN_TOKEN" = "null" ]; then + echo "Error: Failed to get admin token from Tinybird API after $MAX_RETRIES attempts. Please ensure Tinybird is properly configured." >&2 + echo "Tokens response: $TOKENS_RESPONSE" >&2 + exit 1 +fi + +echo "Successfully found admin token with ADMIN scope" + +# Get the tracker token from the same response +TRACKER_TOKEN=$(echo "$TOKENS_RESPONSE" | jq -r '.tokens[] | select(.name == "tracker") | .token') + +# Check if tracker token is valid +if [ -z "$TRACKER_TOKEN" ] || [ "$TRACKER_TOKEN" = "null" ]; then + echo "Error: Failed to get tracker token from Tinybird API. Please ensure Tinybird is properly configured." >&2 + exit 1 +fi + +# Write environment variables to .env file +ENV_FILE="/mnt/shared-config/.env.tinybird" +TMP_ENV_FILE="/mnt/shared-config/.env.tinybird.tmp" + +echo "Writing Tinybird configuration to $ENV_FILE..." + +cat > "$TMP_ENV_FILE" << EOF +TINYBIRD_WORKSPACE_ID=$WORKSPACE_ID +TINYBIRD_ADMIN_TOKEN=$ADMIN_TOKEN +TINYBIRD_TRACKER_TOKEN=$TRACKER_TOKEN +EOF + +if [ $? -eq 0 ]; then + mv "$TMP_ENV_FILE" "$ENV_FILE" + if [ $? -eq 0 ]; then + echo "Successfully wrote Tinybird configuration to $ENV_FILE" + else + echo "Error: Failed to move temporary file to $ENV_FILE" >&2 + exit 1 + fi +else + echo "Error: Failed to create temporary configuration file" >&2 + rm -f "$TMP_ENV_FILE" + exit 1 +fi + +exec "$@" diff --git a/docker/watch-admin-apps.js b/docker/watch-admin-apps.js new file mode 100755 index 0000000..1ed25ad --- /dev/null +++ b/docker/watch-admin-apps.js @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const chokidar = require('chokidar'); +const path = require('path'); + +// Colors for output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m' +}; + +// App configurations - now only need paths, colors, and nx project names +const apps = { + shade: { path: 'apps/shade', color: colors.white, nxName: '@tryghost/shade' }, + design: { path: 'apps/admin-x-design-system', color: colors.cyan, nxName: '@tryghost/admin-x-design-system' }, + framework: { path: 'apps/admin-x-framework', color: colors.magenta, nxName: '@tryghost/admin-x-framework' }, + activitypub: { path: 'apps/admin-x-activitypub', color: colors.red, nxName: '@tryghost/admin-x-activitypub' }, + settings: { path: 'apps/admin-x-settings', color: colors.green, nxName: '@tryghost/admin-x-settings' }, + posts: { path: 'apps/posts', color: colors.yellow, nxName: '@tryghost/posts' }, + stats: { path: 'apps/stats', color: colors.blue, nxName: '@tryghost/stats' } +}; + +// Track all child processes and watchers for cleanup +const activeProcesses = new Set(); +const activeWatchers = new Set(); + +function log(appName, message) { + const app = apps[appName]; + console.log(`${app.color}[${appName}]${colors.reset} ${message}`); +} + +function buildAllProjects(triggerApp) { + return new Promise((resolve) => { + log(triggerApp, 'Running nx run-many to rebuild all projects...'); + const allProjects = Object.values(apps).map(app => app.nxName).join(','); + + const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], { + cwd: '/home/ghost', + stdio: 'pipe', + env: { + ...process.env, + NX_DAEMON: 'false' + } + }); + + activeProcesses.add(child); + + child.stdout.on('data', (data) => { + data.toString().split('\n').forEach(line => { + if (line.trim()) log(triggerApp, `nx: ${line}`); + }); + }); + + child.stderr.on('data', (data) => { + data.toString().split('\n').forEach(line => { + if (line.trim()) log(triggerApp, `nx: ${line}`); + }); + }); + + child.on('close', (code) => { + activeProcesses.delete(child); + if (code === 0) { + log(triggerApp, 'All builds complete'); + } else { + log(triggerApp, `Some builds failed with code ${code}`); + } + resolve(); + }); + + child.on('error', (error) => { + activeProcesses.delete(child); + log(triggerApp, `Build error: ${error.message}`); + resolve(); + }); + }); +} + +function startWatching() { + const watchPaths = Object.values(apps).map(app => path.join('/home/ghost', app.path, 'src')); + + console.log('Watching all project src folders for changes...'); + + const watcher = chokidar.watch(watchPaths, { + persistent: true, + ignoreInitial: true, + usePolling: true, + interval: 1000 + }); + + // Track the watcher for cleanup + activeWatchers.add(watcher); + + let rebuildTimer; + + watcher.on('all', (event, filePath) => { + const relativePath = path.relative('/home/ghost', filePath); + + // Find which project changed for better logging + const changedProject = Object.keys(apps).find(name => + filePath.includes(apps[name].path) + ) || 'unknown'; + + log(changedProject, `Change detected: ${event} ${relativePath}`); + + // Debounce rebuilds + clearTimeout(rebuildTimer); + rebuildTimer = setTimeout(async () => { + await buildAllProjects(changedProject); + }, 500); + }); + + watcher.on('error', (error) => { + console.log(`Watcher error: ${error.message}`); + console.log('Exiting process - Docker will restart the service'); + process.exit(1); + }); + + watcher.on('close', () => { + console.log('Watcher closed unexpectedly'); + console.log('Exiting process - Docker will restart the service'); + process.exit(1); + }); + + return watcher; +} + +async function main() { + console.log('Starting admin apps build and watch system...'); + + try { + // Phase 1: Build everything with nx handling dependency order and parallelization + const allProjects = Object.values(apps).map(app => app.nxName).join(','); + + const child = spawn('pnpm', ['nx', 'run-many', '-t', 'build', `--projects=${allProjects}`], { + cwd: '/home/ghost', + stdio: 'pipe', + env: { + ...process.env, + NX_DAEMON: 'false' + } + }); + + activeProcesses.add(child); + + child.stdout.on('data', (data) => { + data.toString().split('\n').forEach(line => { + if (line.trim()) console.log(`[nx] ${line}`); + }); + }); + + child.stderr.on('data', (data) => { + data.toString().split('\n').forEach(line => { + if (line.trim()) console.log(`[nx] ${line}`); + }); + }); + + await new Promise((resolve, reject) => { + child.on('close', (code) => { + activeProcesses.delete(child); + if (code === 0) { + console.log('\nAll projects built successfully!'); + resolve(); + } else { + console.log(`\nSome builds failed, but continuing with watch processes...`); + resolve(); // Don't crash the watch system if some builds fail + } + }); + + child.on('error', (error) => { + activeProcesses.delete(child); + reject(error); + }); + }); + + // Phase 2: Start single watcher for all projects + // Single watcher for all projects - any change triggers nx run-many (with caching) + const watcher = startWatching(); + + console.log('\nAll watch processes started. Press Ctrl+C to stop.'); + + // Keep the process alive + await new Promise(() => { }); + + } catch (error) { + console.error('Failed to start:', error.message); + process.exit(1); + } +} + +// Graceful shutdown handler +function cleanup() { + console.log('\nShutting down...'); + + // Kill all active child processes + for (const child of activeProcesses) { + try { + child.kill('SIGTERM'); + // Force kill after 1 second if still running + setTimeout(() => { + if (!child.killed) { + child.kill('SIGKILL'); + } + }, 1000); + } catch (error) { + // Process might already be dead + } + } + + // Close all watchers + for (const watcher of activeWatchers) { + try { + watcher.close(); + } catch (error) { + // Watcher might already be closed + } + } + + console.log('Cleanup complete.'); + process.exit(0); +} + +// Handle various termination signals +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); +process.on('SIGQUIT', cleanup); + +// Handle uncaught exceptions to ensure cleanup +process.on('uncaughtException', (error) => { + console.error('Uncaught exception:', error); + cleanup(); +}); + +process.on('unhandledRejection', (reason) => { + console.error('Unhandled rejection:', reason); + cleanup(); +}); + +main(); diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f5c1583 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,121 @@ +# Ghost Contributor Documentation + +Welcome to the Ghost contributor documentation! This guide will help you understand the codebase, set up your development environment, and start contributing to Ghost. + +## Quick Start + +### Prerequisites + +- **Node.js** - Recommended to install via [nvm](https://github.com/nvm-sh/nvm) +- **pnpm** - Package manager +- **Docker** - For MySQL database and development services + +### Initial Setup + +#### 1. Fork and Clone + +First, [fork the Ghost repository](https://github.com/TryGhost/Ghost/fork) on GitHub, then: + +```bash +# Clone your fork with submodules +git clone --recurse-submodules git@github.com:/Ghost.git +cd Ghost + +# Configure remotes +git remote rename origin upstream +git remote add origin git@github.com:/Ghost.git +``` + +#### 2. Install and Setup + +```bash +# Install dependencies and initialize submodules +corepack enable pnpm +pnpm run setup +``` + +#### 3. Start Ghost + +```bash +# Start development (runs Docker backend services + frontend dev servers) +pnpm dev +``` + +Ghost will be available at: +- **Main site**: http://localhost:2368/ +- **Admin panel**: http://localhost:2368/ghost/ + +### Troubleshooting Setup + +If you encounter issues during setup: + +```bash +# Fix dependency issues +pnpm fix + +# Update to latest main branch +pnpm main + +# Reset running dev data +pnpm reset:data +``` + +## Repository Structure + +``` +Ghost/ +├── apps/ # Frontend applications +│ ├── admin-x-*/ # New React-based admin apps +│ ├── portal/ # Member portal +│ ├── comments-ui/ # Comments widget +│ ├── signup-form/ # Signup form widget +│ └── ... +├── ghost/ # Core Ghost application +│ ├── core/ # Main Ghost backend +│ ├── admin/ # Admin build output +│ └── i18n/ # Internationalization +├── e2e/ # End-to-end tests +├── adr/ # Architecture Decision Records +``` + +## Contributing + +Before contributing, please read: + +1. [Contributing Guide](../.github/CONTRIBUTING.md) - Guidelines for contributions +2. [Code of Conduct](../.github/CODE_OF_CONDUCT.md) - Community standards + +### Finding Issues to Work On + +- [Good First Issues](https://github.com/TryGhost/Ghost/labels/good%20first%20issue) - Great for newcomers +- [Help Wanted](https://github.com/TryGhost/Ghost/labels/help%20wanted) - Issues that need attention + +### Development Workflow + +1. **Fork and clone** the repository +2. **Create a branch** for your changes +3. **Make your changes** and write tests +4. **Run tests** to ensure everything works +5. **Commit** following our commit message conventions +6. **Submit a pull request** to the `main` branch + +## Additional Resources + +- **[Official Documentation](https://ghost.org/docs/)** - User and developer docs +- **[Ghost Forum](https://forum.ghost.org)** - Community support and discussions +- **[API Documentation](https://ghost.org/docs/content-api/)** - Content and Admin API reference +- **[Theme Documentation](https://ghost.org/docs/themes/)** - Theme development + +## Architecture Decision Records + +The [adr/](../adr/) directory contains Architecture Decision Records (ADRs) that document significant architectural decisions made in the project. + +## Getting Help + +- **Forum**: [forum.ghost.org](https://forum.ghost.org) +- **Support**: [See SUPPORT.md](../.github/SUPPORT.md) +- **Issues**: [GitHub Issues](https://github.com/TryGhost/Ghost/issues) + +## License + +Ghost is open source software licensed under the [MIT License](../LICENSE). diff --git a/e2e/.claude/E2E_TEST_WRITING_GUIDE.md b/e2e/.claude/E2E_TEST_WRITING_GUIDE.md new file mode 100644 index 0000000..915c4bd --- /dev/null +++ b/e2e/.claude/E2E_TEST_WRITING_GUIDE.md @@ -0,0 +1,498 @@ +# E2E Test Writing Guide + +## Overview +This guide provides instructions for writing E2E tests in the `/e2e/` directory using TypeScript and Playwright. Tests follow the Page Object Model pattern and utilize data factories for test data management. + +## Environment Setup + +### Running Tests +```bash +# From the e2e directory +cd e2e + +# Run all tests +pnpm test + +# Run specific test file +pnpm test tests/admin/feature.test.ts + +# Run with visible browser (debugging) +pnpm test --debug + +# Run with specific timeout +pnpm test --timeout=60000 + +# Keep environment running after test (useful for Playwright MCP exploration) +PRESERVE_ENV=true pnpm test + +# Enable debug logging +DEBUG=@tryghost/e2e:* pnpm test +``` + +## Test Organization + +### Directory Structure +``` +e2e/ +├── tests/ +│ ├── admin/ # Admin panel tests +│ ├── public/ # Public site tests +│ └── [area]/ # Other test areas +├── helpers/ +│ ├── pages/ # Page Objects +│ │ ├── admin/ # Admin page objects +│ │ └── public/ # Public page objects +│ └── playwright/ # Test fixtures and setup +└── data-factory/ # Test data generators +``` + +### Test File Naming +- Test files: `[PageName].test.ts` - Named after the page being tested (e.g., `PostEditor.test.ts`, `MembersList.test.ts`) +- Page objects: `[Feature]Page.ts` (PascalCase) +- Use descriptive names that clearly indicate what's being tested + +## Page Object Pattern + +### Core Principles +1. **ALL selectors must be in Page Objects** - Never put selectors in test files +2. **Page Objects encapsulate page structure and interactions** +3. **Reuse existing Page Objects when possible** +4. **Create focused, single-responsibility Page Objects** + +### Creating a Page Object + +```typescript +// e2e/helpers/pages/admin/FeaturePage.ts +import {Page, Locator} from '@playwright/test'; +import {AdminPage} from './AdminPage'; + +export class FeaturePage extends AdminPage { + // Define locators as readonly properties + readonly elementName: Locator; + readonly buttonName: Locator; + readonly modalDialog: Locator; + + constructor(page: Page) { + super(page); + this.pageUrl = '/ghost/#/[path]'; + + // Selector priority (use in this order): + // 1. data-testid + this.elementName = page.getByTestId('element-id'); + + // 2. ARIA roles with accessible names + this.buttonName = page.getByRole('button', {name: 'Button Text'}); + + // 3. Labels for form elements + this.elementName = page.getByLabel('Field Label'); + + // 4. Text content (for unique text) + this.elementName = page.getByText('Unique text'); + + // 5. Avoid CSS/XPath selectors unless absolutely necessary + } + + // Action methods + async performAction(): Promise { + await this.buttonName.click(); + } + + async fillForm(data: {field1: string; field2: string}): Promise { + await this.field1Input.fill(data.field1); + await this.field2Input.fill(data.field2); + } + + // State verification methods + async isElementVisible(): Promise { + return await this.elementName.isVisible(); + } + + async getElementText(): Promise { + return await this.elementName.textContent() || ''; + } + + // Common utility methods (add to AdminPage or BasePage for reuse) + async pressEscape(): Promise { + await this.page.keyboard.press('Escape'); + } + + async waitForAutoSave(): Promise { + await this.page.waitForFunction(() => { + const status = document.querySelector('[data-test="status"]'); + return status?.textContent?.includes('Saved'); + }); + } +} +``` + +### Modal/Dialog Pattern + +```typescript +export class FeatureModal { + private readonly page: Page; + readonly modal: Locator; + readonly closeButton: Locator; + readonly saveButton: Locator; + + constructor(page: Page) { + this.page = page; + this.modal = page.getByRole('dialog'); + this.closeButton = this.modal.getByRole('button', {name: 'Close'}); + this.saveButton = this.modal.getByRole('button', {name: 'Save'}); + } + + async waitForVisible(): Promise { + await this.modal.waitFor({state: 'visible'}); + } + + async waitForHidden(): Promise { + await this.modal.waitFor({state: 'hidden'}); + } + + async close(): Promise { + await this.closeButton.click(); + await this.waitForHidden(); + } + + async isVisible(): Promise { + return await this.modal.isVisible(); + } +} +``` + +### Extending Base Pages + +```typescript +// Admin pages extend AdminPage +export class PostEditorPage extends AdminPage { + // Implementation +} + +// Public pages extend BasePage +export class PublicHomePage extends BasePage { + // Implementation +} +``` + +## Writing Tests + +### Test Structure (AAA Pattern) + +**Important: Write self-documenting tests without comments. Test names and method names should clearly express intent. If complex logic is needed, extract it to a well-named method in the Page Object.** + +Tests should follow the **Arrange-Act-Assert (AAA)** pattern: +- **Arrange**: Set up test data and page objects +- **Act**: Perform the actions being tested +- **Assert**: Verify the expected outcomes + +The structure should be visually clear through spacing, not comments: + +```typescript +import {test, expect} from '../../helpers/playwright'; +import {FeaturePage} from '../../helpers/pages/admin/FeaturePage'; +import {createPostFactory} from '../../data-factory'; + +test.describe('Feature Name', () => { + test('should perform expected behavior', async ({page, ghostInstance}) => { + // Arrange + const featurePage = new FeaturePage(page); + const postFactory = createPostFactory(page.request); + const post = await postFactory.create({title: 'Test Post'}); + + // Act + await featurePage.goto(); + await featurePage.performAction(); + + // Assert + expect(await featurePage.isElementVisible()).toBe(true); + expect(await featurePage.getResultText()).toContain('Expected text'); + }); +}); +``` + +### Test Fixtures + +The `page` fixture provides: +- Pre-authenticated browser session (logged into Ghost admin) +- Automatic cleanup after test + +The `ghostInstance` fixture provides: +- `baseUrl`: The URL of the Ghost instance +- `database`: Database name for this test +- `port`: Port number the instance is running on + +Additional standalone fixtures exported from `helpers/playwright/fixture.ts` and re-exported by `@/helpers/playwright`: +- `resolvedIsolation`: `'per-file' | 'per-test'` +- `resetEnvironment()`: force a full environment recycle in per-file mode before stateful fixtures are resolved + +```typescript +test.beforeEach(async ({resetEnvironment, resolvedIsolation}) => { + if (resolvedIsolation === 'per-file') { + await resetEnvironment(); + } +}); +``` + +Isolation rules: +- Default is per-file isolation, so the underlying Ghost environment can be reused across tests in the same file. +- Call `usePerTestIsolation()` at the root of a file to switch to per-test isolation and force a fresh Ghost environment for each test. +- Import it from `@/helpers/playwright/isolation`. +- `config` and `labs` participate in the per-file environment identity. If either changes, the shared environment is recycled. +- `stripeEnabled` always forces per-test isolation because Ghost must boot against a per-test fake Stripe server. +- `resetEnvironment()` is a hook-only escape hatch. Do not call it after `baseURL`, `page`, `pageWithAuthenticatedUser`, or `ghostAccountOwner` has already been resolved. +- Do not treat `resetEnvironment()` as an in-test cleanup step. If you recycle the environment, you must re-establish any stateful fixtures, and the supported pattern is to call it in `beforeEach` before those fixtures are created. +- ESLint catches direct misuse, but the runtime guard in the fixture is the final enforcement. + +When to use each option: +- `config`: for boot-time Ghost config such as billing URLs or force-upgrade flags. +- `labs`: for tests that need specific labs flags on or off. +- `stripeEnabled`: for tests that need the fake Stripe server and Stripe-backed Ghost boot config. +- `usePerTestIsolation()`: for whole files that mutate shared state heavily and should never reuse a Ghost environment across tests. + +## Data Factories + +### Using Data Factories + +Data factories provide a clean way to create test data. Import the factory you need and use it to generate data with specific attributes. + +```typescript +import {createPostFactory, createMemberFactory} from '../../data-factory'; + +test('test with data', async ({page}) => { + const postFactory = createPostFactory(page.request); + const memberFactory = createMemberFactory(page); + + const post = await postFactory.create({ + title: 'Test Post', + content: 'Test content', + status: 'published' + }); + + const member = await memberFactory.create({ + name: 'Test Member', + email: 'test@example.com' + }); + + const postEditorPage = new PostEditorPage(page); + await postEditorPage.gotoExistingPost(post.id); +}); +``` + +### Factory Pattern +Factories are available for various Ghost entities. Check the `data-factory` directory for available factories. Common examples include: +- Creating posts with different statuses and content +- Creating members with subscriptions +- Creating staff users with specific roles +- Creating tags, offers, and other entities + +New factories are added as needed. When you need test data that doesn't have a factory yet, consider creating one rather than manually constructing the data. + +## Best Practices + +### DO's +✅ **Use Page Objects for all selectors** +✅ **Write self-documenting tests** with clear method and test names +✅ **Check existing Page Objects before creating new ones** +✅ **Use proper waits** (`waitForLoadState`, `waitFor`, etc.) +✅ **Keep tests isolated** - Each test gets its own Ghost instance +✅ **Use descriptive test names** that explain what's being tested +✅ **Extract complex logic to well-named methods** in Page Objects +✅ **Use data factories** for complex test data +✅ **Add meaningful assertions** beyond just visibility checks + +### DON'Ts +❌ **Never put selectors in test files** +❌ **Don't write comments** - make code self-documenting instead +❌ **Don't use hardcoded waits** (`page.waitForTimeout`) +❌ **Don't use networkidle in waits** (`page.waitForLoadState('networkidle')`) - rely on web assertions to assess readiness instead +❌ **Don't depend on test execution order** +❌ **Don't manually log in** - use the pre-authenticated fixture +❌ **Avoid CSS/XPath selectors** - use semantic selectors +❌ **Don't create test data manually** if a factory exists + +## Common Patterns + +### Waiting for Elements + +```typescript +// Good - explicit waits +await element.waitFor({state: 'visible'}); +await page.waitForSelector('[data-test="element"]'); + +// Bad - arbitrary timeouts +await page.waitForTimeout(5000); // Avoid this! +``` + +### Handling Async Operations + +```typescript +// Wait for save to complete +await page.waitForFunction(() => { + const status = document.querySelector('[data-test="status"]'); + return status?.textContent?.includes('Saved'); +}); +``` + +### Working with iframes + +```typescript +// Access iframe content +const iframe = page.locator('iframe[title="preview"]'); +const frameContent = iframe.contentFrame(); +await frameContent.click('button'); +``` + +### Keyboard Shortcuts + +```typescript +// Press keyboard keys +await page.keyboard.press('Escape'); +await page.keyboard.press('Control+S'); +await page.keyboard.type('Hello World'); +``` + +## Ghost-Specific Patterns + +### Common Selectors +- Navigation: `data-test-nav="[section]"` +- Buttons: `data-test-button="[action]"` +- Lists: `data-test-list="[name]"` +- Modals: `[role="dialog"]` or `.gh-modal` +- Loading states: `.gh-loading-spinner` + +### Admin URLs +- Editor: `/ghost/#/editor/post/[id]` +- Posts list: `/ghost/#/posts` +- Settings: `/ghost/#/settings` +- Members: `/ghost/#/members` + +### Common UI Elements +- Buttons: `.gh-btn-[color]` (e.g., `.gh-btn-primary`) +- Inputs: Often use `name` or `placeholder` attributes +- Status indicators: `[data-test="status"]` + +## Using Playwright MCP for Page Object Discovery + +When creating new Page Objects or discovering selectors for unfamiliar UI: + +### 1. Start Ghost with Preserved Environment +```bash +# Start Ghost and keep it running +PRESERVE_ENV=true pnpm test + +# The test will output the Ghost instance URL (usually http://localhost:2369) +``` + +### 2. Use Playwright MCP to Explore +```javascript +// Navigate to the Ghost instance +mcp__playwright__browser_navigate({url: "http://localhost:2369/ghost"}) + +// Capture the current DOM structure +mcp__playwright__browser_snapshot() + +// Interact with elements to discover selectors +mcp__playwright__browser_click({element: "Button description", ref: "selector-from-snapshot"}) + +// Take screenshots for reference +mcp__playwright__browser_take_screenshot({filename: "feature-state.png"}) +``` + +### 3. Extract Selectors for Page Objects +Based on your exploration, create the Page Object with discovered selectors: +- Note the element references from snapshots +- Identify the best selector strategy (testId, role, label, text) +- Test interactions before finalizing the Page Object + +## Debugging + +### Debug Mode +```bash +# See browser while test runs +pnpm test --debug + +# UI mode for interactive debugging +pnpm test --ui +``` + +### Debug Logging +```bash +# Enable all e2e debug logs +DEBUG=@tryghost/e2e:* pnpm test + +# Specific debug namespace +DEBUG=@tryghost/e2e:ghost-fixture pnpm test +``` + +### Preserve Environment +```bash +# Keep containers running after test +PRESERVE_ENV=true pnpm test +``` + +### Test Artifacts +- Screenshots on failure: `test-results/` +- Playwright traces: `test-results/` + +## Test Isolation + +Each test automatically gets: +1. **Fresh Ghost instance** with unique database +2. **Unique port** to avoid conflicts +3. **Pre-authenticated session** +4. **Automatic cleanup** after test completion + +You don't need to worry about: +- Database cleanup +- Port conflicts +- Login/logout +- Test data pollution + +## Validation Checklist + +Before submitting a test: +- [ ] All selectors are in Page Objects +- [ ] Test follows AAA pattern +- [ ] Test is deterministic (not flaky) +- [ ] Uses proper waits (no arbitrary timeouts) +- [ ] Has meaningful assertions +- [ ] Follows naming conventions +- [ ] Reuses existing Page Objects where possible +- [ ] Test passes locally +- [ ] Test fails for the right reason (if demonstrating a bug) + +## Quick Reference + +### Essential Imports +```typescript +import {test, expect} from '../../helpers/playwright'; +import {PageName} from '../../helpers/pages/admin/PageName'; +import {createPostFactory} from '../../data-factory'; +``` + +### Test Template +```typescript +test.describe('Feature', () => { + test('specific behavior', async ({page, ghostInstance}) => { + // Arrange + const pageObject = new PageObject(page); + + // Act + await pageObject.goto(); + await pageObject.action(); + + // Assert + expect(await pageObject.getState()).toBe(expected); + }); +}); +``` + +### Run Commands +```bash +pnpm test # All tests +pnpm test path/to/test.ts # Specific test +pnpm test --debug # With browser +pnpm test --grep "pattern" # Pattern matching +PRESERVE_ENV=true pnpm test # Keep environment +DEBUG=@tryghost/e2e:* pnpm test # Debug logs +``` diff --git a/e2e/.env.example b/e2e/.env.example new file mode 100644 index 0000000..48ef1bc --- /dev/null +++ b/e2e/.env.example @@ -0,0 +1,7 @@ +# Debug failed tests (keeps containers) +PRESERVE_ENV=false + +# define only if you want custom number of workers for tests to run in parallel locally +TEST_WORKERS_COUNT=5 +# Skips the docker build pretest command, if you don't need to rebuild the container +GHOST_E2E_SKIP_BUILD=1 diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md new file mode 100644 index 0000000..0e0d880 --- /dev/null +++ b/e2e/AGENTS.md @@ -0,0 +1,150 @@ +# AGENTS.md + +E2E testing guidance for AI assistants (Claude, Codex, etc.) working with Ghost tests. + +**IMPORTANT**: When creating or modifying E2E tests, always refer to `.claude/E2E_TEST_WRITING_GUIDE.md` for comprehensive testing guidelines and patterns. + +## Critical Rules +1. **Always follow ADRs** in `../adr/` folder (ADR-0001: AAA pattern, ADR-0002: Page Objects) +2. **Always use pnpm**, never npm +3. **Always run after changes**: `pnpm lint` and `pnpm test:types` +4. **Never use CSS/XPath selectors** - only semantic locators or data-testid +5. **Prefer less comments and giving things clear names** + +## Running E2E Tests + +**`pnpm dev` must be running before you run E2E tests.** The E2E test runner auto-detects +whether the admin dev server is reachable at `http://127.0.0.1:5174`. If it is, tests run +in **dev mode** (fast, no pre-built Docker image required). If not, tests fall back to +**build mode** which requires a `ghost-e2e:local` Docker image that is only built in CI. + +**If you see the error `Build image not found: ghost-e2e:local`, it means `pnpm dev` is +not running.** Start it first, wait for the admin dev server to be ready, then re-run tests. + +```bash +# Terminal 1 (or background): Start dev environment from the repo root +pnpm dev + +# Wait for the admin dev server to be reachable (http://127.0.0.1:5174) + +# Terminal 2: Run e2e tests from the e2e/ directory +pnpm test # Run all tests +pnpm test tests/path/to/test.ts # Run specific test +pnpm lint # Required after writing tests +pnpm test:types # Check TypeScript errors +pnpm build # Required after factory changes +pnpm test --debug # See browser during execution, for debugging +PRESERVE_ENV=true pnpm test # Debug failed tests (keeps containers) +``` +## Test Structure + +### Naming Conventions +- **Test suites**: `Ghost Admin - Feature` or `Ghost Public - Feature` +- **Test names**: `what is tested - expected outcome` (lowercase) +- **One test = one scenario** (never mix multiple scenarios) + +### AAA Pattern +```typescript +test('action performed - expected result', async ({page}) => { + const analyticsPage = new AnalyticsGrowthPage(page); + const postFactory = createPostFactory(page.request); + const post = await postFactory.create({status: 'published'}); + + await analyticsPage.goto(); + await analyticsPage.topContent.postsButton.click(); + + await expect(analyticsPage.topContent.contentCard).toContainText('No conversions'); +}); +``` + +## Page Objects + +### Structure +```typescript +export class AnalyticsPage extends AdminPage { + // Public readonly locators only + public readonly saveButton = this.page.getByRole('button', {name: 'Save'}); + public readonly emailInput = this.page.getByLabel('Email'); + + // Semantic action methods + async saveSettings() { + await this.saveButton.click(); + } +} +``` + +### Rules +- Page Objects are located in `helpers/pages/` +- Expose locators as `public readonly` when used with assertions +- Methods use semantic names (`login()` not `clickLoginButton()`) +- Use `waitFor()` for guards, never `expect()` in page objects +- Keep all assertions in test files + +## Locators (Strict Priority) + +1. **Semantic** (always prefer): + - `getByRole('button', {name: 'Save'})` + - `getByLabel('Email')` + - `getByText('Success')` + +2. **Test IDs** (when semantic unavailable): + - `getByTestId('analytics-card')` + - Suggest adding `data-testid` to Ghost codebase when needed + +3. **Never use**: CSS selectors, XPath, nth-child, class names + +### Playwright MCP Usage +- Use `mcp__playwright__browser_snapshot` to find elements +- Use `mcp__playwright__browser_click` with semantic descriptions +- If no good locator exists, suggest `data-testid` addition to Ghost + +## Test Data + +### Factory Pattern (Required) +```typescript +import {PostFactory, UserFactory} from '../data-factory'; + +const postFactory = createPostFactory(page.request); +const post = await postFactory.create({userId: user.id}); +``` + +## Best Practices + +### DO ✅ +- Use `usePerTestIsolation()` from `@/helpers/playwright/isolation` if a file needs per-test isolation +- Treat `config` and `labs` as environment-identity inputs: changing them should be an intentional part of test setup +- Use `resetEnvironment()` only in `beforeEach` hooks when you need a forced recycle inside per-file mode +- Keep `stripeEnabled` tests in per-test mode; the fixture forces this automatically +- Use factories for all test data +- Use Playwright's auto-waiting +- Run tests multiple times to ensure stability +- Use `test.only()` for debugging single tests + +### DON'T ❌ +- Use `test.describe.parallel(...)` or `test.describe.serial(...)` in e2e tests +- Use nested `test.describe.configure({mode: ...})` (mode toggles are root-level only) +- Call `resetEnvironment()` after resolving `baseURL`, `page`, `pageWithAuthenticatedUser`, or `ghostAccountOwner` +- Hard-coded waits (`waitForTimeout`) +- networkidle in waits (`networkidle`) +- Test dependencies (Test B needs Test A) +- Direct database manipulation +- Multiple scenarios in one test +- Assertions in page objects +- Manual login (auto-authenticated via fixture) + +## Project Structure +- `tests/admin/` - Admin area tests +- `tests/public/` - Public site tests +- `helpers/pages/` - Page objects +- `helpers/environment/` - Container management +- `data-factory/` - Test data factories + +## Validation Checklist +After writing tests, verify: +1. Test passes: `pnpm test path/to/test.ts` +2. Linting passes: `pnpm lint` +3. Types check: `pnpm test:types` +4. Follows AAA pattern with clear sections +5. Uses page objects appropriately +6. Uses semantic locators or data-testid only +7. No hard-coded waits or CSS selectors diff --git a/e2e/CLAUDE.md b/e2e/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/e2e/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/e2e/Dockerfile.e2e b/e2e/Dockerfile.e2e new file mode 100644 index 0000000..277a3bd --- /dev/null +++ b/e2e/Dockerfile.e2e @@ -0,0 +1,26 @@ +# E2E test layer: copies locally-built public apps into Ghost's content folder +# so Ghost serves them from /content/files/* (same origin, no CORS). +# +# Usage: +# docker build -f e2e/Dockerfile.e2e \ +# --build-arg GHOST_IMAGE=ghost-monorepo:latest \ +# -t ghost-e2e:local . +# +# Intended for the production Ghost image built in CI. + +ARG GHOST_IMAGE=ghost-monorepo:latest +FROM $GHOST_IMAGE + +# Public app UMD bundles — Ghost serves these from /content/files/ +COPY apps/portal/umd /home/ghost/content/files/portal +COPY apps/comments-ui/umd /home/ghost/content/files/comments-ui +COPY apps/sodo-search/umd /home/ghost/content/files/sodo-search +COPY apps/signup-form/umd /home/ghost/content/files/signup-form +COPY apps/announcement-bar/umd /home/ghost/content/files/announcement-bar + +ENV portal__url=/content/files/portal/portal.min.js +ENV comments__url=/content/files/comments-ui/comments-ui.min.js +ENV sodoSearch__url=/content/files/sodo-search/sodo-search.min.js +ENV sodoSearch__styles=/content/files/sodo-search/main.css +ENV signupForm__url=/content/files/signup-form/signup-form.min.js +ENV announcementBar__url=/content/files/announcement-bar/announcement-bar.min.js diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..7ade4c0 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,300 @@ +# Ghost End-To-End Test Suite + +This test suite runs automated browser tests against a running Ghost instance to ensure critical user journeys work correctly. + +## Quick Start + +### Prerequisites +- Docker and Docker Compose installed +- Node.js installed (pnpm is managed via corepack — run `corepack enable pnpm` first) + +### Running Tests +To run the test, within this `e2e` folder run: + +```bash +# Install dependencies +pnpm + +# All tests +pnpm test +``` + +### Dev Environment Mode (Recommended for Development) + +If `GHOST_E2E_MODE` is unset, the e2e shell entrypoints auto-select: +- `dev` when the local admin dev server is reachable on `http://127.0.0.1:5174` +- `build` otherwise + +To use dev mode, start `pnpm dev` before running tests: + +```bash +# Terminal 1: Start dev environment (from repository root) +pnpm dev + +# Terminal 2: Run e2e tests (from e2e folder) +pnpm test +``` + +If infra is already running, `pnpm infra:up` is safe to run again. +For dev-mode test runs, `infra:up` also ensures required local Ghost/gateway dev images exist. +If you want to force a mode, set `GHOST_E2E_MODE=dev` or `GHOST_E2E_MODE=build` explicitly. + +### Analytics Development Flow + +When working on analytics locally, use: + +```bash +# Terminal 1 (repo root) +pnpm dev:analytics + +# Terminal 2 +pnpm test:analytics +``` + +E2E test scripts automatically sync Tinybird tokens when Tinybird is running. + +### Build Mode (Prebuilt Image) + +Use build mode when you don’t want to run dev servers. It uses a prebuilt Ghost image and serves public assets from `/content/files`. + +```bash +# From repository root +pnpm build +pnpm --filter @tryghost/e2e build:apps +GHOST_E2E_BASE_IMAGE= pnpm --filter @tryghost/e2e build:docker +GHOST_E2E_MODE=build pnpm --filter @tryghost/e2e infra:up + +# Run tests +GHOST_E2E_MODE=build GHOST_E2E_IMAGE=ghost-e2e:local pnpm --filter @tryghost/e2e test +``` + +For a CI-like local preflight (pulls Playwright + gateway images and starts infra), run: + +```bash +pnpm --filter @tryghost/e2e preflight:build +``` + + +### Running Specific Tests + +```bash +# Specific test file +pnpm test specific/folder/testfile.spec.ts + +# Matching a pattern +pnpm test --grep "homepage" + +# With browser visible (for debugging) +pnpm test --debug +``` + +## Tests Development + +The test suite is organized into separate directories for different areas/functions: + +### **Current Test Suites** +- `tests/public/` - Public-facing site tests (homepage, posts, etc.) +- `tests/admin/` - Ghost admin panel tests (login, content creation, settings) + +We can decide whether to add additional sub-folders as we add more tests. + +Example structure for admin tests: +```text +tests/admin/ +├── login.spec.ts +├── posts.spec.ts +└── settings.spec.ts +``` + +Project folder structure can be seen below: + +```text +e2e/ +├── tests/ # All the tests +│ ├── public/ # Public site tests +│ │ └── testname.spec.ts # Test cases +│ ├── admin/ # Admin site tests +│ │ └── testname.spec.ts # Test cases +│ ├── global.setup.ts # Global setup script +│ ├── global.teardown.ts # Global teardown script +│ └── .eslintrc.js # Test-specific ESLint config +├── helpers/ # All helpers that support the tests, utilities, fixtures, page objects etc. +│ ├── playwright/ # Playwright specific helpers +│ │ └── fixture.ts # Playwright fixtures +│ ├── pages/ # Page Object Models +│ │ └── HomePage.ts # Page Object +│ ├── utils/ # Utils +│ │ └── math.ts # Math related utils +│ └── index.ts # Main exports +├── playwright.config.mjs # Playwright configuration +├── package.json # Dependencies and scripts +└── tsconfig.json # TypeScript configuration +``` + +### Writing Tests + +Tests use [Playwright Test](https://playwright.dev/docs/writing-tests) framework with page objects. +Aim to format tests in Arrange Act Assert style - it will help you with directions when writing your tests. + +```typescript +test.describe('Ghost Homepage', () => { + test('loads correctly', async ({page}) => { + // ARRANGE - setup fixtures, create helpers, prepare things that helps will need to be executed + const homePage = new HomePage(page); + + // ACT - do the actions you need to do, to verify certain behaviour + await homePage.goto(); + + // ASSERT + await expect(homePage.title).toBeVisible(); + }); +}); +``` + +### Using Page Objects + +Page objects encapsulate page elements, and interactions. To read more about them, check [this link out](https://www.selenium.dev/documentation/test_practices/encouraged/page_object_models/) and [this link](https://martinfowler.com/bliki/PageObject.html). + +```typescript +// Create a page object for admin login +export class AdminLoginPage { + private pageUrl:string; + + constructor(private page: Page) { + this.pageUrl = '/ghost' + } + + async goto(urlToVisit = this.pageUrl) { + await this.page.goto(urlToVisit); + } + + async login(email: string, password: string) { + await this.page.fill('[name="identification"]', email); + await this.page.fill('[name="password"]', password); + await this.page.click('button[type="submit"]'); + } +} +``` + +### Global Setup and Teardown + +Tests use [Project Dependencies](https://playwright.dev/docs/test-global-setup-teardown#option-1-project-dependencies) to define special tests as global setup and teardown tests: + +- Global Setup: `tests/global.setup.ts` - runs once before all tests +- Global Teardown: `tests/global.teardown.ts` - runs once after all tests + +### Playwright Fixtures + +[Playwright Fixtures](https://playwright.dev/docs/test-fixtures) are defined in `helpers/playwright/fixture.ts` and provide reusable test setup/teardown logic. +The fixture resolves isolation mode per test file: +- Default: per-file isolation (one Ghost environment cycle per file) +- Opt-in per-test: call `usePerTestIsolation()` from `@/helpers/playwright/isolation` at the root of the file +- Forced per-test: any run with `fullyParallel: true` + +### Test Isolation + +Test isolation is still automatic, but no longer always per-test. + +Infrastructure (MySQL, Redis, Mailpit, Tinybird) must already be running before tests start. Use `pnpm dev` or `pnpm --filter @tryghost/e2e infra:up`. + +Global setup (`tests/global.setup.ts`) does: +- Cleans up e2e containers and test databases +- Creates a base database, starts Ghost, waits for health, snapshots the DB + +Per-file mode (`helpers/playwright/fixture.ts`) does: +- Clones a new database from snapshot at file boundary +- Restarts Ghost with the new database and waits for readiness +- Reuses that environment for tests in the file + +Per-test mode (`helpers/playwright/fixture.ts`) does: +- Clones a new database from snapshot for each test +- Restarts Ghost with the new database and waits for readiness + +Environment identity for per-file reuse: +- `config` participates in the environment identity. +- `labs` participates in the environment identity. +- If either changes between tests in the same file, the shared per-file Ghost environment is recycled before reuse. +- `stripeEnabled` does not participate in per-file reuse. It always forces per-test isolation because Ghost must boot against a per-test fake Stripe server. + +Fixture option behavior: +- `config`: use for boot-time Ghost config that should get a fresh environment when it changes. +- `labs`: use for labs flags that should get a fresh environment when they change. +- `stripeEnabled`: use for Stripe-backed tests; this always runs each test with a fully isolated Ghost environment. + +Escape hatch: +- `resetEnvironment()` is supported only in `beforeEach` hooks for per-file tests. +- Use it only before resolving stateful fixtures such as `baseURL`, `page`, `pageWithAuthenticatedUser`, or `ghostAccountOwner`. +- Safe hook pattern: `test.beforeEach(async ({resetEnvironment}) => { ... })` +- Unsupported pattern: calling `resetEnvironment()` after `page` or an authenticated session has already been created. +- ESLint catches the obvious misuse cases, but the runtime guard in the fixture remains the hard safety check. + +Opting into per-test isolation: +- Use `usePerTestIsolation()` from `@/helpers/playwright/isolation` at the root of the file. +- This configures both Playwright parallel mode and the fixture isolation in one call. + +Global teardown (`tests/global.teardown.ts`) does: +- Cleans up e2e containers and test databases (infra services stay running) + +Modes: +- Dev mode: Ghost mounts source code and proxies assets to host dev servers +- Build mode: Ghost uses a prebuilt image and serves assets from `/content/files` + +### Best Practices + +1. **Use page object patterns** to separate page elements, actions on the pages, complex logic from tests. They should help you make them more readable and UI elements reusable. +2. **Add meaningful assertions** beyond just page loads. Keep assertions in tests. +3. **Use `data-testid` attributes** for reliable element selection, in case you **cannot** locate elements in a simple way. Example: `page.getByLabel('User Name')`. Avoid, css, xpath locators - they make tests brittle. +4. **Clean up test data** when tests modify Ghost state +5. **Group related tests** in describe blocks +6. **Do not use should to describe test scenarios** + +## CI Integration + +Tests run automatically in GitHub Actions on every PR and commit to `main`. + +### CI Process + +1. **Setup**: Ubuntu runner with Node.js and Docker +2. **Build Assets**: Build server/admin assets and public app UMD bundles +3. **Build E2E Image**: `pnpm --filter @tryghost/e2e build:docker` (layers public apps into `/content/files`) +4. **Prepare E2E Runtime**: Pull Playwright/gateway images in parallel, start infra, and sync Tinybird state (`pnpm --filter @tryghost/e2e preflight:build`) +5. **Test Execution**: Run Playwright E2E tests inside the official Playwright container +6. **Artifacts**: Upload Playwright traces and reports on failure + +## Available Scripts + +Within the e2e directory: + +```bash +# Run all tests +pnpm test + +# Start/stop test infra (MySQL/Redis/Mailpit/Tinybird) +pnpm infra:up +pnpm infra:down + +# CI-like preflight for build mode (pulls images + starts infra) +pnpm preflight:build + +# Debug failed tests (keeps containers) +PRESERVE_ENV=true pnpm test + +# Run TypeScript type checking +pnpm test:types + +# Lint code and tests +pnpm lint + +# Build (for utilities) +pnpm build +pnpm dev # Watch mode for TypeScript compilation +``` + +## Resolving issues + +### Test Failures + +1. **Screenshots**: Playwright captures screenshots on failure +2. **Traces**: Available in `test-results/` directory +3. **Debug Mode**: Run with `pnpm test --debug` or `pnpm test --ui` to see browser +4. **Verbose Logging**: Check CI logs for detailed error information diff --git a/e2e/data-factory/README.md b/e2e/data-factory/README.md new file mode 100644 index 0000000..6731b76 --- /dev/null +++ b/e2e/data-factory/README.md @@ -0,0 +1,115 @@ +# Ghost Data Factory + +A minimal test data factory for Ghost e2e tests, written in TypeScript. + +## Project Structure + +``` +e2e/data-factory/ # Source files (TypeScript) - committed to git +├── factory.ts # Base factory class +├── factories/ # Factory implementations +│ ├── post-factory.ts +│ ├── tag-factory.ts +│ └── user-factory.ts +├── persistence/ +│ ├── adapter.ts # Persistence interface +│ └── adapters/ # Adapter implementations (API, Knex, etc) +├── setup.ts # Setup helper functions +├── index.ts # Main exports +└── utils.ts # Utility functions +``` + +## Setup + +This is part of the Ghost e2e test suite. All dependencies are managed by the main Ghost monorepo. + +1. **Start Ghost development server** (provides database): + ```bash + pnpm dev + ``` + +2. **Configure database connection** (optional - uses Ghost's database by default): + ```bash + cp e2e/data-factory/.env.example e2e/data-factory/.env + # Edit .env if using different database credentials + ``` + +3. **Build the e2e package** (includes data-factory): + ```bash + cd e2e && pnpm build + ``` + +## Usage + +### In Tests + +**Option 1: Use setup helpers (recommended)** +```typescript +import {createPostFactory, PostFactory} from '../data-factory'; + +// Create factory with API persistence +const postFactory: PostFactory = createPostFactory(page.request); + +// Build in-memory only (not persisted) +const draftPost = postFactory.build({ + title: 'My Draft', + status: 'draft' +}); + +// Create and persist to database +const publishedPost = await postFactory.create({ + title: 'My Published Post', + status: 'published' +}); +``` + +**Option 2: Manual setup** +```typescript +import {PostFactory} from '../data-factory/factories/post-factory'; +import {GhostAdminApiAdapter} from '../data-factory/persistence/adapters/ghost-api'; + +const adapter = new GhostAdminApiAdapter(page.request, 'posts'); +const postFactory = new PostFactory(adapter); + +// Now you can build or create +const post = await postFactory.create({ + title: 'My Published Post', + status: 'published' +}); +``` + +## Development + +### Adding New Factories + +1. Create a new factory class extending `Factory` +2. Implement the `build()` method (returns in-memory object) +3. Set `entityType` property (used for persistence) +4. Create a setup helper in `setup.ts` for convenient usage in tests + +Example: +```typescript +import {Factory} from '../factory'; + +export class MemberFactory extends Factory, Member> { + entityType = 'members'; + + build(options: Partial = {}): Member { + return { + id: generateId(), + email: options.email || faker.internet.email(), + name: options.name || faker.person.fullName(), + // ... more fields + }; + } +} +``` + +Then create a setup helper: +```typescript +// In setup.ts +export function createMemberFactory(httpClient: HttpClient): MemberFactory { + const adapter = new GhostAdminApiAdapter(httpClient, 'members'); + return new MemberFactory(adapter); +} +``` diff --git a/e2e/data-factory/factories/automated-email-factory.ts b/e2e/data-factory/factories/automated-email-factory.ts new file mode 100644 index 0000000..bb9b42e --- /dev/null +++ b/e2e/data-factory/factories/automated-email-factory.ts @@ -0,0 +1,60 @@ +import {Factory} from '@/data-factory'; +import {generateId} from '@/data-factory'; + +export interface AutomatedEmail { + id: string; + status: 'active' | 'inactive'; + name: string; + slug: string; + subject: string; + lexical: string; + sender_name: string | null; + sender_email: string | null; + sender_reply_to: string | null; + created_at: Date; + updated_at: Date | null; +} + +export class AutomatedEmailFactory extends Factory, AutomatedEmail> { + entityType = 'automated_emails'; + + build(options: Partial = {}): AutomatedEmail { + const now = new Date(); + + const defaults: AutomatedEmail = { + id: generateId(), + status: 'active', + name: 'Welcome Email (Free)', + slug: 'member-welcome-email-free', + subject: 'Welcome to {site_title}!', + lexical: JSON.stringify(this.defaultLexicalContent()), + sender_name: null, + sender_email: null, + sender_reply_to: null, + created_at: now, + updated_at: null + }; + + return {...defaults, ...options} as AutomatedEmail; + } + + private defaultLexicalContent() { + return { + root: { + children: [{ + type: 'paragraph', + children: [{ + type: 'text', + text: 'Welcome to {site_title}!' + }] + }], + direction: null, + format: '', + indent: 0, + type: 'root', + version: 1 + } + }; + } +} + diff --git a/e2e/data-factory/factories/comment-factory.ts b/e2e/data-factory/factories/comment-factory.ts new file mode 100644 index 0000000..df4f0c7 --- /dev/null +++ b/e2e/data-factory/factories/comment-factory.ts @@ -0,0 +1,32 @@ +import {Factory} from '@/data-factory'; +import {faker} from '@faker-js/faker'; +import {generateId} from '@/data-factory'; + +export interface Comment { + id: string; + post_id: string; + member_id: string; + parent_id?: string; + in_reply_to_id?: string; + status: 'published' | 'hidden' | 'deleted'; + html: string; + created_at?: string; + edited_at?: string; +} + +export class CommentFactory extends Factory, Comment> { + entityType = 'comments'; + + build(options: Partial = {}): Comment { + const content = options.html || `

${faker.lorem.sentence()}

`; + + return { + id: generateId(), + post_id: options.post_id || '', + member_id: options.member_id || '', + status: 'published', + html: content, + ...options + }; + } +} diff --git a/e2e/data-factory/factories/lexical.ts b/e2e/data-factory/factories/lexical.ts new file mode 100644 index 0000000..4ea3adc --- /dev/null +++ b/e2e/data-factory/factories/lexical.ts @@ -0,0 +1,108 @@ +import {faker} from '@faker-js/faker'; + +interface LexicalTextNode { + detail: number; + format: number; + mode: string; + style: string; + text: string; + type: 'text'; + version: number; +} + +interface LexicalParagraphNode { + children: LexicalTextNode[]; + direction: string; + format: string; + indent: number; + type: 'paragraph'; + version: number; +} + +interface CardNode { + type: string; + [key: string]: unknown; +} + +const CARD_DEFAULTS: Record = { + transistor: { + type: 'transistor', + version: 1, + accentColor: '#15171A', + backgroundColor: '#FFFFFF', + visibility: { + web: { + nonMember: false, + memberSegment: 'status:free,status:-free' + }, + email: { + memberSegment: 'status:free,status:-free' + } + } + } +}; + +export type CardSpec = string | {[cardType: string]: Record}; + +function resolveCard(spec: CardSpec): CardNode { + if (typeof spec === 'string') { + const defaults = CARD_DEFAULTS[spec]; + if (!defaults) { + throw new Error(`Unknown card type: "${spec}". Register it in CARD_DEFAULTS in lexical.ts.`); + } + return {...defaults}; + } + + const [cardType, overrides] = Object.entries(spec)[0]; + const defaults = CARD_DEFAULTS[cardType]; + if (!defaults) { + throw new Error(`Unknown card type: "${cardType}". Register it in CARD_DEFAULTS in lexical.ts.`); + } + return {...defaults, ...overrides}; +} + +function buildParagraphNode(text: string): LexicalParagraphNode { + return { + children: [{ + detail: 0, + format: 0, + mode: 'normal', + style: '', + text, + type: 'text', + version: 1 + }], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1 + }; +} + +export function buildLexical(...cards: CardSpec[]): string { + let children: (LexicalParagraphNode | CardNode)[]; + + if (cards.length === 0) { + children = [buildParagraphNode(faker.lorem.paragraphs(3))]; + } else { + children = []; + for (const spec of cards) { + const card = resolveCard(spec); + children.push(buildParagraphNode(`Before ${card.type}`)); + children.push(card); + children.push(buildParagraphNode(`After ${card.type}`)); + } + } + + return JSON.stringify({ + root: { + children, + direction: 'ltr', + format: '', + indent: 0, + type: 'root', + version: 1 + } + }); +} diff --git a/e2e/data-factory/factories/member-factory.ts b/e2e/data-factory/factories/member-factory.ts new file mode 100644 index 0000000..2940415 --- /dev/null +++ b/e2e/data-factory/factories/member-factory.ts @@ -0,0 +1,68 @@ +import {Factory} from '@/data-factory'; +import {faker} from '@faker-js/faker'; +import {generateId, generateUuid} from '@/data-factory'; + +export interface Tier { + id: string; + name: string; + slug: string; + type: 'free' | 'paid'; + active: boolean; +} + +export interface Member { + id: string; + uuid: string; + name: string | null; + email: string; + note?: string | null; + geolocation: string | null; + labels?: string[]; + email_count: number; + email_opened_count: number; + email_open_rate: number | null; + status: 'free' | 'paid' | 'comped'; + last_seen_at: Date | null; + last_commented_at: Date | null; + newsletters: string[]; + tiers?: Partial[]; + created_at?: string; // ISO 8601 format for backdating + complimentary_plan?: boolean; + stripe_customer_id?: string; + subscribed_to_emails?: string; +} + +export class MemberFactory extends Factory, Member> { + entityType = 'members'; + + build(options: Partial = {}): Member { + return { + ...this.buildDefaultMember(), + ...options + }; + } + + private buildDefaultMember(): Member { + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + const name = `${firstName} ${lastName}`; + + return { + id: generateId(), + uuid: generateUuid(), + name: name, + email: faker.internet.email({firstName, lastName}).toLowerCase(), + note: faker.lorem.sentence(), + geolocation: null, + labels: [], + email_count: 0, + email_opened_count: 0, + email_open_rate: null, + status: 'free', + last_seen_at: null, + last_commented_at: null, + newsletters: [], + subscribed_to_emails: 'false' + }; + } +} diff --git a/e2e/data-factory/factories/offer-factory.ts b/e2e/data-factory/factories/offer-factory.ts new file mode 100644 index 0000000..e3b95f9 --- /dev/null +++ b/e2e/data-factory/factories/offer-factory.ts @@ -0,0 +1,169 @@ +import {Factory, generateId, generateSlug} from '@/data-factory'; +import {faker} from '@faker-js/faker'; +import type {HttpClient, PersistenceAdapter} from '@/data-factory'; + +export interface AdminOffer { + id: string; + name: string; + code: string; + cadence: 'month' | 'year'; + redemption_type?: 'signup' | 'retention'; + status: 'active' | 'archived'; + display_title: string | null; + display_description: string | null; + type: 'fixed' | 'percent' | 'trial'; + amount: number; + duration: 'once' | 'repeating' | 'forever' | 'trial'; + duration_in_months: number | null; + currency: string | null; + stripe_coupon_id?: string | null; + tier: { + id: string; + } | null; +} + +export interface OfferCreateInput { + [key: string]: unknown; + name: string; + code: string; + cadence: 'month' | 'year'; + amount: number; + duration: 'once' | 'repeating' | 'forever' | 'trial'; + type: 'fixed' | 'percent' | 'trial'; + tierId?: string | null; + currency?: string | null; + display_title?: string | null; + display_description?: string | null; + duration_in_months?: number | null; + redemption_type?: 'signup' | 'retention'; + status?: 'active' | 'archived'; +} + +export interface OfferUpdateInput { + status?: 'active' | 'archived'; +} + +export class OfferFactory extends Factory { + entityType = 'offers'; + private readonly request?: HttpClient; + + constructor(adapter?: PersistenceAdapter, request?: HttpClient) { + super(adapter); + this.request = request; + } + + build(options: Partial = {}): AdminOffer { + const name = options.name ?? `Offer ${faker.commerce.productName()}`; + const code = options.code ?? `${generateSlug(name)}-${faker.string.alphanumeric(6).toLowerCase()}`; + const redemptionType = options.redemption_type ?? (options.tierId ? 'signup' : 'retention'); + + return { + id: generateId(), + name, + code, + cadence: options.cadence ?? 'month', + redemption_type: redemptionType, + status: options.status ?? 'active', + display_title: options.display_title ?? name, + display_description: options.display_description ?? null, + type: options.type ?? 'percent', + amount: options.amount ?? 10, + duration: options.duration ?? 'once', + duration_in_months: options.duration_in_months ?? null, + currency: options.currency ?? null, + stripe_coupon_id: null, + tier: options.tierId ? {id: options.tierId} : null + }; + } + + async create(options: Partial = {}): Promise { + if (!this.request) { + throw new Error('Cannot create without an HTTP client. Use createOfferFactory() for persisted test data access.'); + } + + const offer = this.build(options); + const response = await this.request.post('/ghost/api/admin/offers', { + data: { + offers: [{ + name: offer.name, + code: offer.code, + cadence: offer.cadence, + status: offer.status, + redemption_type: offer.redemption_type ?? 'signup', + currency: offer.currency, + type: offer.type, + amount: offer.amount, + duration: offer.duration, + duration_in_months: offer.duration_in_months, + display_title: offer.display_title, + display_description: offer.display_description, + tier: offer.tier + }] + } + }); + + return await this.extractFirstOfferOrThrow('create offer', response.status(), response); + } + + async update(id: string, input: OfferUpdateInput): Promise { + if (!this.request) { + throw new Error('Cannot update without an HTTP client. Use createOfferFactory() for persisted test data access.'); + } + + const response = await this.request.put(`/ghost/api/admin/offers/${id}`, { + data: { + offers: [input] + } + }); + + return await this.extractFirstOfferOrThrow('update offer', response.status(), response); + } + + async getOffers(): Promise { + if (!this.request) { + throw new Error('Cannot fetch offers without an HTTP client. Use createOfferFactory() for persisted test data access.'); + } + + const response = await this.request.get('/ghost/api/admin/offers'); + if (!response.ok()) { + throw new Error(`Failed to fetch offers: ${response.status()}`); + } + + const data = await response.json() as {offers: AdminOffer[]}; + return data.offers; + } + + async getById(id: string): Promise { + if (!this.request) { + throw new Error('Cannot fetch an offer without an HTTP client. Use createOfferFactory() for persisted test data access.'); + } + + const response = await this.request.get(`/ghost/api/admin/offers/${id}`); + return await this.extractFirstOfferOrThrow('fetch offer', response.status(), response); + } + + private async extractFirstOfferOrThrow(action: string, status: number, response: {ok(): boolean; json(): Promise}): Promise { + if (!response.ok()) { + throw new Error(`Failed to ${action}: ${status}`); + } + + const data = await response.json() as {offers?: AdminOffer[]}; + const offers = data.offers; + + if (!Array.isArray(offers) || offers.length === 0) { + let responseBody = '[unserializable]'; + + try { + responseBody = JSON.stringify(data); + } catch { + // Ignore serialization errors and keep fallback marker. + } + + throw new Error( + `Failed to ${action}: expected response.offers to be a non-empty array (status ${status}). Response: ${responseBody}` + ); + } + + return offers[0]; + } +} diff --git a/e2e/data-factory/factories/post-factory.ts b/e2e/data-factory/factories/post-factory.ts new file mode 100644 index 0000000..fb486b8 --- /dev/null +++ b/e2e/data-factory/factories/post-factory.ts @@ -0,0 +1,89 @@ +import {Factory} from '@/data-factory'; +import {buildLexical} from './lexical'; +import {faker} from '@faker-js/faker'; +import {generateId, generateSlug, generateUuid} from '@/data-factory'; +import type {CardSpec} from './lexical'; + +export interface Post { + id: string; + uuid: string; + title: string; + slug: string; + mobiledoc: string | null; + lexical: string | null; + html: string; + comment_id: string; + plaintext: string; + feature_image: string | null; + featured: boolean; + type: string; + status: 'draft' | 'published' | 'scheduled'; + locale: string | null; + visibility: string; + email_recipient_filter: string; + created_at: Date; + updated_at: Date; + published_at: Date | null; + custom_excerpt: string; + codeinjection_head: string | null; + codeinjection_foot: string | null; + custom_template: string | null; + canonical_url: string | null; + newsletter_id: string | null; + show_title_and_feature_image: boolean; + tags?: Array<{id: string}>; + tiers?: Array<{id: string}>; +} + +export class PostFactory extends Factory, Post> { + entityType = 'posts'; // Entity name (for adapter; currently API endpoint) + + build(options: Partial = {}): Post { + const now = new Date(); + const title = options.title || faker.lorem.sentence(); + const content = faker.lorem.paragraphs(3); + + const defaults: Post = { + id: generateId(), + uuid: generateUuid(), + title: title, + slug: options.slug || generateSlug(title) + '-' + Date.now().toString(16), + mobiledoc: null, + lexical: buildLexical(), + html: `

${content}

`, + comment_id: generateId(), + plaintext: content, + feature_image: null, + featured: faker.datatype.boolean(), + type: 'post', + status: 'draft', + locale: null, + visibility: 'public', + email_recipient_filter: 'none', + created_at: now, + updated_at: now, + published_at: null, + custom_excerpt: faker.lorem.paragraph(), + codeinjection_head: null, + codeinjection_foot: null, + custom_template: null, + canonical_url: null, + newsletter_id: null, + show_title_and_feature_image: true, + tags: undefined + }; + + // Determine published_at based on status and user options + let publishedAt = options.published_at ?? defaults.published_at; + if (options.status === 'published' && !options.published_at) { + publishedAt = now; + } + + return {...defaults, ...options, published_at: publishedAt} as Post; + } + + async createWithCards(cards: CardSpec | CardSpec[], options: Partial = {}): Promise { + const cardArray = Array.isArray(cards) ? cards : [cards]; + return this.create({...options, lexical: buildLexical(...cardArray)}); + } +} diff --git a/e2e/data-factory/factories/tag-factory.ts b/e2e/data-factory/factories/tag-factory.ts new file mode 100644 index 0000000..5c67042 --- /dev/null +++ b/e2e/data-factory/factories/tag-factory.ts @@ -0,0 +1,72 @@ +import {Factory} from '@/data-factory'; +import {faker} from '@faker-js/faker'; +import {generateId, generateSlug} from '@/data-factory'; + +export interface Tag { + id: string; + name: string; + slug: string; + description: string | null; + feature_image: string | null; + parent_id: string | null; + visibility: 'public' | 'internal'; + url?: string; + og_image: string | null; + og_title: string | null; + og_description: string | null; + twitter_image: string | null; + twitter_title: string | null; + twitter_description: string | null; + meta_title: string | null; + meta_description: string | null; + codeinjection_head: string | null; + codeinjection_foot: string | null; + canonical_url: string | null; + accent_color: string | null; + count?: { + posts: number; + }; + created_at: Date; + updated_at: Date | null; +} +export class TagFactory extends Factory, Tag> { + entityType = 'tags'; + + build(options: Partial = {}): Tag { + return { + ...this.buildDefaultTag(), + ...options + }; + } + + private buildDefaultTag(): Tag { + const now = new Date(); + const tagName = faker.commerce.department(); + + return { + id: generateId(), + name: tagName, + slug: `${generateSlug(tagName)}-${faker.string.alphanumeric(6).toLowerCase()}`, + description: faker.lorem.sentence(), + feature_image: `https://picsum.photos/seed/tag-${faker.string.alphanumeric(8)}/1200/630`, + parent_id: null, + visibility: 'public', + url: undefined, + og_image: null, + og_title: null, + og_description: faker.lorem.sentence(), + twitter_image: null, + twitter_title: null, + twitter_description: faker.lorem.sentence(), + meta_title: null, + meta_description: faker.lorem.sentence(), + codeinjection_head: null, + codeinjection_foot: null, + canonical_url: null, + accent_color: null, + count: {posts: 0}, + created_at: now, + updated_at: now + }; + } +} diff --git a/e2e/data-factory/factories/tier-factory.ts b/e2e/data-factory/factories/tier-factory.ts new file mode 100644 index 0000000..ee5fa77 --- /dev/null +++ b/e2e/data-factory/factories/tier-factory.ts @@ -0,0 +1,83 @@ +import {Factory} from '@/data-factory'; +import {faker} from '@faker-js/faker'; +import {generateId, generateSlug} from '@/data-factory'; +import type {HttpClient, PersistenceAdapter} from '@/data-factory'; +import type {Tier} from './member-factory'; + +export interface AdminTier extends Tier { + description?: string | null; + visibility?: 'public' | 'none'; + welcome_page_url?: string | null; + benefits?: string[] | null; + currency?: string; + monthly_price?: number; + yearly_price?: number; + trial_days?: number; + created_at?: Date; + updated_at?: Date | null; +} + +export interface TierCreateInput { + name: string; + description?: string; + visibility?: 'public' | 'none'; + welcome_page_url?: string; + benefits?: string[]; + currency: string; + monthly_price: number; + yearly_price: number; + trial_days?: number; +} + +export class TierFactory extends Factory, AdminTier> { + entityType = 'tiers'; + private readonly request?: HttpClient; + + constructor(adapter?: PersistenceAdapter, request?: HttpClient) { + super(adapter); + this.request = request; + } + + build(options: Partial = {}): AdminTier { + const tierName = options.name ?? `Tier ${faker.commerce.productName()}`; + const now = new Date(); + + const defaults: AdminTier = { + id: generateId(), + name: tierName, + slug: `${generateSlug(tierName)}-${faker.string.alphanumeric(6).toLowerCase()}`, + type: 'paid', + active: true, + description: faker.lorem.sentence(), + visibility: 'public', + welcome_page_url: null, + benefits: [], + currency: 'usd', + monthly_price: 500, + yearly_price: 5000, + trial_days: 0, + created_at: now, + updated_at: now + }; + + return {...defaults, ...options}; + } + + async getFirstPaidTier(): Promise { + if (!this.request) { + throw new Error('Cannot fetch tiers without an HTTP client. Use createTierFactory() for persisted test data access.'); + } + + const response = await this.request.get('/ghost/api/admin/tiers'); + if (!response.ok()) { + throw new Error(`Failed to fetch tiers: ${response.status()}`); + } + + const {tiers} = await response.json() as {tiers: AdminTier[]}; + const paidTier = tiers.find(tier => tier.type === 'paid' && tier.active); + if (!paidTier) { + throw new Error('No paid tiers found'); + } + return paidTier; + } +} diff --git a/e2e/data-factory/factories/user-factory.ts b/e2e/data-factory/factories/user-factory.ts new file mode 100644 index 0000000..a6eaaff --- /dev/null +++ b/e2e/data-factory/factories/user-factory.ts @@ -0,0 +1,26 @@ +import {Factory} from '@/data-factory'; + +export interface User { + name: string; + email: string; + password: string; + blogTitle: string; +} + +export class UserFactory extends Factory, User> { + entityType = 'users'; + + public build(overrides: Partial = {}): User { + return { + ...this.defaults, + ...overrides + }; + } + + private defaults: User = { + name: 'Test Admin', + email: 'test@example.com', + password: 'test123', + blogTitle: 'Test Blog' + }; +} diff --git a/e2e/data-factory/factory.ts b/e2e/data-factory/factory.ts new file mode 100644 index 0000000..2b63c2a --- /dev/null +++ b/e2e/data-factory/factory.ts @@ -0,0 +1,38 @@ +import type {PersistenceAdapter} from './persistence/adapter'; + +export abstract class Factory = Record, TResult = TOptions> { + abstract entityType: string; + + protected adapter?: PersistenceAdapter; + + constructor(adapter?: PersistenceAdapter) { + this.adapter = adapter; + } + + abstract build(options?: Partial): TResult; + + buildMany(optionsList: Partial[]): TResult[] { + return optionsList.map(options => this.build(options)); + } + + async create(options?: Partial): Promise { + if (!this.adapter) { + throw new Error('Cannot create without a persistence adapter. Use build() for in-memory objects.'); + } + const data = this.build(options); + return await this.adapter.insert(this.entityType, data) as Promise; + } + + async createMany(optionsList: Partial[]): Promise { + if (!this.adapter) { + throw new Error('Cannot create without a persistence adapter. Use buildMany() for in-memory objects.'); + } + + const results: TResult[] = []; + for (const options of optionsList) { + const result = await this.create(options); + results.push(result); + } + return results; + } +} diff --git a/e2e/data-factory/index.ts b/e2e/data-factory/index.ts new file mode 100644 index 0000000..db1a137 --- /dev/null +++ b/e2e/data-factory/index.ts @@ -0,0 +1,37 @@ +// Core Factory exports +export {Factory} from './factory'; +export {PostFactory} from './factories/post-factory'; +export type {Post} from './factories/post-factory'; +export {TagFactory} from './factories/tag-factory'; +export type {Tag} from './factories/tag-factory'; +export {MemberFactory} from './factories/member-factory'; +export type {Member, Tier} from './factories/member-factory'; +export {TierFactory} from './factories/tier-factory'; +export type {AdminTier, TierCreateInput} from './factories/tier-factory'; +export {OfferFactory} from './factories/offer-factory'; +export type {AdminOffer, OfferCreateInput, OfferUpdateInput} from './factories/offer-factory'; +export {AutomatedEmailFactory} from './factories/automated-email-factory'; +export type {AutomatedEmail} from './factories/automated-email-factory'; +export {CommentFactory} from './factories/comment-factory'; +export type {Comment} from './factories/comment-factory'; +export * from './factories/user-factory'; + +// Persistence Adapters +export {KnexPersistenceAdapter} from './persistence/adapters/knex'; +export {ApiPersistenceAdapter} from './persistence/adapters/api'; +export type {HttpClient, HttpResponse} from './persistence/adapters/http-client'; +export {GhostAdminApiAdapter} from './persistence/adapters/ghost-api'; +export type {PersistenceAdapter} from './persistence/adapter'; + +// Utilities +export {generateId, generateUuid, generateSlug} from './utils'; + +// Factory Setup Helpers +export {createPostFactory} from './setup'; +export {createTagFactory} from './setup'; +export {createMemberFactory} from './setup'; +export {createTierFactory} from './setup'; +export {createOfferFactory} from './setup'; +export {createAutomatedEmailFactory} from './setup'; +export {createCommentFactory} from './setup'; +export {createFactories} from './setup'; diff --git a/e2e/data-factory/persistence/adapter.ts b/e2e/data-factory/persistence/adapter.ts new file mode 100644 index 0000000..0fd032d --- /dev/null +++ b/e2e/data-factory/persistence/adapter.ts @@ -0,0 +1,12 @@ +/** + * Core persistence adapter interface + */ +export interface PersistenceAdapter { + insert(entityType: string, data: T): Promise; + update(entityType: string, id: string, data: Partial): Promise; + delete(entityType: string, id: string): Promise; + findById(entityType: string, id: string): Promise; + // Optional methods - implement as needed + deleteMany?(entityType: string, ids: string[]): Promise; + findMany?(entityType: string, query?: Record): Promise; +} \ No newline at end of file diff --git a/e2e/data-factory/setup.ts b/e2e/data-factory/setup.ts new file mode 100644 index 0000000..5fea354 --- /dev/null +++ b/e2e/data-factory/setup.ts @@ -0,0 +1,102 @@ +import {AutomatedEmailFactory} from './factories/automated-email-factory'; +import {CommentFactory} from './factories/comment-factory'; +import {GhostAdminApiAdapter} from './persistence/adapters/ghost-api'; +import {HttpClient} from './persistence/adapters/http-client'; +import {MemberFactory} from './factories/member-factory'; +import {OfferFactory} from './factories/offer-factory'; +import {PostFactory} from './factories/post-factory'; +import {TagFactory} from './factories/tag-factory'; +import {TierFactory} from './factories/tier-factory'; + +/** + * Create a new PostFactory with API persistence + * Uses the http client which already has the proper authentication headers and baseURL + * configured (this would be Playwright's page.request) + * + * @param httpClient - client for requests with pre-defined authorization and base url + * @returns PostFactory ready to use with the specified Ghost backend + */ +export function createPostFactory(httpClient: HttpClient): PostFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'posts', + {formats: 'mobiledoc,lexical,html'} + ); + return new PostFactory(adapter); +} + +export function createTagFactory(httpClient: HttpClient): TagFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'tags' + ); + return new TagFactory(adapter); +} + +export function createMemberFactory(httpClient: HttpClient): MemberFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'members' + ); + return new MemberFactory(adapter); +} + +export function createTierFactory(httpClient: HttpClient): TierFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'tiers' + ); + return new TierFactory(adapter, httpClient); +} + +export function createOfferFactory(httpClient: HttpClient): OfferFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'offers' + ); + return new OfferFactory(adapter, httpClient); +} + +export function createAutomatedEmailFactory(httpClient: HttpClient): AutomatedEmailFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'automated_emails' + ); + return new AutomatedEmailFactory(adapter); +} + +export function createCommentFactory(httpClient: HttpClient): CommentFactory { + const adapter = new GhostAdminApiAdapter( + httpClient, + 'comments' + ); + return new CommentFactory(adapter); +} + +export interface Factories { + postFactory: PostFactory; + tagFactory: TagFactory; + memberFactory: MemberFactory; + tierFactory: TierFactory; + offerFactory: OfferFactory; + automatedEmailFactory: AutomatedEmailFactory; + commentFactory: CommentFactory; +} + +/** + * Helper for creating all factories with the same http client + * @param httpClient - client for requests with pre-defined authorization and base url + * + * @returns All factories ready to use with the specified Ghost backend + */ +export function createFactories(httpClient: HttpClient): Factories { + return { + postFactory: createPostFactory(httpClient), + tagFactory: createTagFactory(httpClient), + memberFactory: createMemberFactory(httpClient), + tierFactory: createTierFactory(httpClient), + offerFactory: createOfferFactory(httpClient), + automatedEmailFactory: createAutomatedEmailFactory(httpClient), + commentFactory: createCommentFactory(httpClient) + }; +} diff --git a/e2e/data-factory/utils.ts b/e2e/data-factory/utils.ts new file mode 100644 index 0000000..7c44808 --- /dev/null +++ b/e2e/data-factory/utils.ts @@ -0,0 +1,25 @@ +import {faker} from '@faker-js/faker'; +import {randomBytes} from 'crypto'; + +/** + * Generate a MongoDB-style ObjectId + */ +export function generateId(): string { + const timestamp = Math.floor(Date.now() / 1000).toString(16); + const randomHex = randomBytes(8).toString('hex'); + return timestamp + randomHex; +} + +export function generateUuid(): string { + return faker.string.uuid(); +} + +export function generateSlug(text: string): string { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/--+/g, '-') + .replace(/^-+/, '') + .replace(/-+$/, ''); +} diff --git a/e2e/eslint.config.js b/e2e/eslint.config.js new file mode 100644 index 0000000..56caeb9 --- /dev/null +++ b/e2e/eslint.config.js @@ -0,0 +1,234 @@ +import eslint from '@eslint/js'; +import ghostPlugin from 'eslint-plugin-ghost'; +import playwrightPlugin from 'eslint-plugin-playwright'; +import tseslint from 'typescript-eslint'; +import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths' + +const resetEnvironmentStaleFixtures = ['baseURL', 'ghostAccountOwner', 'page', 'pageWithAuthenticatedUser']; + +function isBeforeEachHookCall(node) { + if (node.type !== 'CallExpression') { + return false; + } + + if (node.callee.type === 'Identifier') { + return node.callee.name === 'beforeEach'; + } + + return node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'beforeEach'; +} + +function isFunctionNode(node) { + return node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration'; +} + +function getDestructuredFixtureNames(functionNode) { + const [firstParam] = functionNode.params; + if (!firstParam || firstParam.type !== 'ObjectPattern') { + return new Set(); + } + + const fixtureNames = new Set(); + for (const property of firstParam.properties) { + if (property.type !== 'Property') { + continue; + } + + if (property.key.type === 'Identifier') { + fixtureNames.add(property.key.name); + } + } + + return fixtureNames; +} + +const noUnsafeResetEnvironment = { + meta: { + type: 'problem', + docs: { + description: 'Restrict resetEnvironment() to supported beforeEach hooks' + }, + messages: { + invalidLocation: 'resetEnvironment() is only supported inside beforeEach hooks. Use a beforeEach hook or switch the file to usePerTestIsolation().', + invalidFixtures: 'Do not resolve {{fixtures}} in the same beforeEach hook as resetEnvironment(); those fixtures become stale after a recycle.' + } + }, + create(context) { + return { + CallExpression(node) { + if (isBeforeEachHookCall(node)) { + const callback = node.arguments.find(argument => isFunctionNode(argument)); + if (!callback) { + return; + } + + const fixtureNames = getDestructuredFixtureNames(callback); + if (!fixtureNames.has('resetEnvironment')) { + return; + } + + const staleFixtures = resetEnvironmentStaleFixtures.filter(fixtureName => fixtureNames.has(fixtureName)); + if (staleFixtures.length > 0) { + context.report({ + node: callback, + messageId: 'invalidFixtures', + data: { + fixtures: staleFixtures.map(fixtureName => `"${fixtureName}"`).join(', ') + } + }); + } + + return; + } + + if (node.callee.type !== 'Identifier' || node.callee.name !== 'resetEnvironment') { + return; + } + + const ancestors = context.sourceCode.getAncestors(node); + const enclosingBeforeEachHook = [...ancestors] + .reverse() + .find((ancestor) => isFunctionNode(ancestor) && + ancestor.parent && + isBeforeEachHookCall(ancestor.parent)); + + if (!enclosingBeforeEachHook) { + context.report({ + node, + messageId: 'invalidLocation' + }); + } + } + }; + } +}; + +const localPlugin = { + rules: { + 'no-unsafe-reset-environment': noUnsafeResetEnvironment + } +}; + +export default tseslint.config([ + // Ignore patterns + { + ignores: [ + 'build/**', + 'data/**', + 'playwright/**', + 'playwright-report/**', + 'test-results/**' + ] + }, + + // Base config for all TypeScript files + { + files: ['**/*.ts', '**/*.mjs'], + extends: [ + eslint.configs.recommended, + tseslint.configs.recommended + ], + languageOptions: { + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module' + } + }, + plugins: { + ghost: ghostPlugin, + playwright: playwrightPlugin, + 'no-relative-import-paths': noRelativeImportPaths, + local: localPlugin, + }, + rules: { + // Manually include rules from plugin:ghost/ts and plugin:ghost/ts-test + // These would normally come from the extends, but flat config requires explicit inclusion + ...ghostPlugin.configs.ts.rules, + + // Sort multiple import lines into alphabetical groups + 'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', { + memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'] + }], + + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + + // Apply no-relative-import-paths rule + 'no-relative-import-paths/no-relative-import-paths': [ + 'error', + { allowSameFolder: true, rootDir: './', prefix: '@' }, + ], + + // Restrict imports to specific directories + 'no-restricted-imports': ['error', { + patterns: ['@/helpers/pages/*'] + }], + + // Disable all mocha rules from ghost plugin since this package uses playwright instead + ...Object.fromEntries( + Object.keys(ghostPlugin.rules || {}) + .filter(rule => rule.startsWith('mocha/')) + .map(rule => [`ghost/${rule}`, 'off']) + ) + } + }, + + // Keep assertions in test files and Playwright-specific helpers. + { + files: ['**/*.ts', '**/*.mjs'], + ignores: [ + 'tests/**/*.ts', + 'helpers/playwright/**/*.ts', + 'visual-regression/**/*.ts' + ], + rules: { + 'no-restricted-syntax': ['error', + { + selector: "ImportSpecifier[imported.name='expect'][parent.source.value='@playwright/test']", + message: 'Keep Playwright expect assertions in test files.' + }, + { + selector: "ImportSpecifier[imported.name='expect'][parent.source.value='@/helpers/playwright']", + message: 'Keep Playwright expect assertions in test files.' + } + ] + } + }, + + // Playwright-specific recommended rules config for test files + { + files: ['tests/**/*.ts', 'helpers/playwright/**/*.ts', 'helpers/pages/**/*.ts'], + rules: { + ...playwrightPlugin.configs.recommended.rules, + 'playwright/expect-expect': ['warn', { + assertFunctionPatterns: ['^expect[A-Z].*'] + }] + } + }, + + // Keep test files on page objects and the supported isolation APIs. + { + files: ['tests/**/*.ts'], + rules: { + 'local/no-unsafe-reset-environment': 'error', + 'no-restricted-syntax': ['error', + { + selector: "CallExpression[callee.object.name='page'][callee.property.name='locator']", + message: 'Use page objects or higher-level page methods instead of page.locator() in test files.' + }, + { + selector: 'MemberExpression[object.property.name="describe"][property.name="parallel"]', + message: 'test.describe.parallel() is deprecated. Use usePerTestIsolation() from @/helpers/playwright/isolation instead.' + }, + { + selector: 'MemberExpression[object.property.name="describe"][property.name="serial"]', + message: 'test.describe.serial() is deprecated. Use test.describe.configure({mode: "serial"}) if needed.' + } + ] + } + } +]); diff --git a/e2e/helpers/environment/constants.ts b/e2e/helpers/environment/constants.ts new file mode 100644 index 0000000..6e42de6 --- /dev/null +++ b/e2e/helpers/environment/constants.ts @@ -0,0 +1,98 @@ +import path from 'path'; +import {fileURLToPath} from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const CONFIG_DIR = path.resolve(__dirname, '../../data/state'); + +// Repository root path (for source mounting and config files) +export const REPO_ROOT = path.resolve(__dirname, '../../..'); + +export const DEV_COMPOSE_PROJECT = process.env.COMPOSE_PROJECT_NAME || 'ghost-dev'; +// compose.dev.yaml pins the network name explicitly, so this does not follow COMPOSE_PROJECT_NAME. +export const DEV_NETWORK_NAME = 'ghost_dev'; +export const DEV_SHARED_CONFIG_VOLUME = `${DEV_COMPOSE_PROJECT}_shared-config`; +export const DEV_PRIMARY_DATABASE = process.env.MYSQL_DATABASE || 'ghost_dev'; + +/** + * Caddyfile paths for different modes. + * - dev: Proxies to host dev servers for HMR + * - build: Minimal passthrough (assets served by Ghost from /content/files/) + */ +export const CADDYFILE_PATHS = { + dev: path.resolve(REPO_ROOT, 'docker/dev-gateway/Caddyfile'), + build: path.resolve(REPO_ROOT, 'docker/dev-gateway/Caddyfile.build') +} as const; + +/** + * Build mode image configuration. + * Used for build mode - can be locally built or pulled from registry. + * + * Override with environment variable: + * - GHOST_E2E_IMAGE: Image name (default: ghost-e2e:local) + * + * Examples: + * - Local: ghost-e2e:local (built from e2e/Dockerfile.e2e) + * - Registry: ghcr.io/tryghost/ghost:latest (as E2E base image) + * - Community: ghost + */ +export const BUILD_IMAGE = process.env.GHOST_E2E_IMAGE || 'ghost-e2e:local'; + +/** + * Build mode gateway image. + * Uses stock Caddy by default so CI does not need a custom gateway build. + */ +export const BUILD_GATEWAY_IMAGE = process.env.GHOST_E2E_GATEWAY_IMAGE || 'caddy:2-alpine'; + +export const TINYBIRD = { + LOCAL_HOST: 'tinybird-local', + PORT: 7181, + JSON_PATH: path.resolve(CONFIG_DIR, 'tinybird.json') +}; + +/** + * Configuration for dev environment mode. + * Used when pnpm dev infrastructure is detected. + */ +export const DEV_ENVIRONMENT = { + projectNamespace: DEV_COMPOSE_PROJECT, + networkName: DEV_NETWORK_NAME +} as const; + +/** + * Base environment variables shared by all modes. + */ +export const BASE_GHOST_ENV = [ + // Environment configuration + 'NODE_ENV=development', + 'server__host=0.0.0.0', + 'server__port=2368', + + // Database configuration (database name is set per container) + 'database__client=mysql2', + 'database__connection__host=ghost-dev-mysql', + 'database__connection__port=3306', + 'database__connection__user=root', + 'database__connection__password=root', + + // Redis configuration + 'adapters__cache__Redis__host=ghost-dev-redis', + 'adapters__cache__Redis__port=6379', + + // Email configuration + 'mail__transport=SMTP', + 'mail__options__host=ghost-dev-mailpit', + 'mail__options__port=1025' +] as const; + +export const TEST_ENVIRONMENT = { + projectNamespace: 'ghost-dev-e2e', + gateway: { + image: `${DEV_COMPOSE_PROJECT}-ghost-dev-gateway` + }, + ghost: { + image: `${DEV_COMPOSE_PROJECT}-ghost-dev`, + port: 2368 + } +} as const; diff --git a/e2e/helpers/environment/environment-factory.ts b/e2e/helpers/environment/environment-factory.ts new file mode 100644 index 0000000..5a5b33e --- /dev/null +++ b/e2e/helpers/environment/environment-factory.ts @@ -0,0 +1,15 @@ +import {EnvironmentManager} from './environment-manager'; + +// Cached manager instance (one per worker process) +let cachedManager: EnvironmentManager | null = null; + +/** + * Get the environment manager for this worker. + * Creates and caches a manager on first call, returns cached instance thereafter. + */ +export async function getEnvironmentManager(): Promise { + if (!cachedManager) { + cachedManager = new EnvironmentManager(); + } + return cachedManager; +} diff --git a/e2e/helpers/environment/environment-manager.ts b/e2e/helpers/environment/environment-manager.ts new file mode 100644 index 0000000..9dd0ca5 --- /dev/null +++ b/e2e/helpers/environment/environment-manager.ts @@ -0,0 +1,160 @@ +import baseDebug from '@tryghost/debug'; +import logging from '@tryghost/logging'; +import {GhostInstance, MySQLManager} from './service-managers'; +import {GhostManager} from './service-managers/ghost-manager'; +import {randomUUID} from 'crypto'; +import type {GhostConfig} from '@/helpers/playwright/fixture'; + +const debug = baseDebug('e2e:EnvironmentManager'); + +/** + * Environment modes for E2E testing. + * + * - dev: Uses dev infrastructure with hot-reloading dev servers + * - build: Uses pre-built image (local or registry, controlled by GHOST_E2E_IMAGE) + */ +export type EnvironmentMode = 'dev' | 'build'; +type GhostEnvOverrides = GhostConfig | Record; + +/** + * Orchestrates e2e test environment. + * + * Supports two modes controlled by GHOST_E2E_MODE environment variable: + * - dev: Uses dev infrastructure with hot-reloading + * - build: Uses pre-built image (set GHOST_E2E_IMAGE for registry images) + * + * All modes use the same infrastructure (MySQL, Redis, Mailpit, Tinybird) + * started via docker compose. Ghost and gateway containers are created + * dynamically per-worker for test isolation. + */ +export class EnvironmentManager { + private readonly mode: EnvironmentMode; + private readonly workerIndex: number; + private readonly mysql: MySQLManager; + private readonly ghost: GhostManager; + private initialized = false; + + constructor() { + this.mode = this.detectMode(); + this.workerIndex = parseInt(process.env.TEST_PARALLEL_INDEX || '0', 10); + + this.mysql = new MySQLManager(); + this.ghost = new GhostManager({ + workerIndex: this.workerIndex, + mode: this.mode + }); + } + + /** + * Detect environment mode from GHOST_E2E_MODE environment variable. + */ + private detectMode(): EnvironmentMode { + const envMode = process.env.GHOST_E2E_MODE; + if (envMode === 'build' || envMode === 'dev') { + return envMode; + } + + logging.warn('GHOST_E2E_MODE is not set; defaulting to build mode. Use the e2e shell entrypoints for automatic mode resolution.'); + return 'build'; + } + + /** + * Global setup - creates database snapshot for test isolation. + * + * Creates the worker 0 containers (Ghost + Gateway) and waits for Ghost to + * become healthy. Ghost automatically runs migrations on startup. Once healthy, + * we snapshot the database for test isolation. + */ + async globalSetup(): Promise { + logging.info(`Starting ${this.mode} environment global setup...`); + + await this.cleanupResources(); + + // Create base database + await this.mysql.recreateBaseDatabase('ghost_e2e_base'); + + // Create containers and wait for Ghost to be healthy (runs migrations) + await this.ghost.setup('ghost_e2e_base'); + await this.ghost.waitForReady(); + this.initialized = true; + + // Snapshot the migrated database for test isolation + await this.mysql.createSnapshot('ghost_e2e_base'); + + logging.info(`${this.mode} environment global setup complete`); + } + + /** + * Global teardown - cleanup resources. + */ + async globalTeardown(): Promise { + if (this.shouldPreserveEnvironment()) { + logging.info('PRESERVE_ENV is set - skipping teardown'); + return; + } + + logging.info(`Starting ${this.mode} environment global teardown...`); + await this.cleanupResources(); + logging.info(`${this.mode} environment global teardown complete`); + } + + /** + * Per-test setup - creates containers on first call, then clones database and restarts Ghost. + */ + async perTestSetup(options: { + config?: GhostEnvOverrides; + stripe?: { + secretKey: string; + publishableKey: string; + }; + } = {}): Promise { + // Lazy initialization of Ghost containers (once per worker) + if (!this.initialized) { + debug('Initializing Ghost containers for worker', this.workerIndex, 'in mode', this.mode); + await this.ghost.setup(); + this.initialized = true; + } + + const siteUuid = randomUUID(); + const instanceId = `ghost_e2e_${siteUuid.replace(/-/g, '_')}`; + + // Setup database + await this.mysql.setupTestDatabase(instanceId, siteUuid, { + stripe: options.stripe + }); + + // Restart Ghost with new database + await this.ghost.restartWithDatabase(instanceId, options.config); + await this.ghost.waitForReady(); + + const port = this.ghost.getGatewayPort(); + + return { + containerId: this.ghost.ghostContainerId!, + instanceId, + database: instanceId, + port, + baseUrl: `http://localhost:${port}`, + siteUuid + }; + } + + /** + * Per-test teardown - drops test database. + */ + async perTestTeardown(instance: GhostInstance): Promise { + await this.mysql.cleanupTestDatabase(instance.database); + } + + private async cleanupResources(): Promise { + logging.info('Cleaning up e2e resources...'); + await this.ghost.cleanupAllContainers(); + await this.mysql.dropAllTestDatabases(); + await this.mysql.deleteSnapshot(); + logging.info('E2E resources cleaned up'); + } + + private shouldPreserveEnvironment(): boolean { + return process.env.PRESERVE_ENV === 'true'; + } +} diff --git a/e2e/helpers/environment/index.ts b/e2e/helpers/environment/index.ts new file mode 100644 index 0000000..70ef4e4 --- /dev/null +++ b/e2e/helpers/environment/index.ts @@ -0,0 +1,3 @@ +export * from './service-managers'; +export * from './environment-manager'; +export * from './environment-factory'; diff --git a/e2e/helpers/environment/service-availability.ts b/e2e/helpers/environment/service-availability.ts new file mode 100644 index 0000000..bd234f1 --- /dev/null +++ b/e2e/helpers/environment/service-availability.ts @@ -0,0 +1,28 @@ +import Docker from 'dockerode'; +import baseDebug from '@tryghost/debug'; +import {DEV_ENVIRONMENT, TINYBIRD} from './constants'; + +const debug = baseDebug('e2e:ServiceAvailability'); + +async function isServiceAvailable(docker: Docker, serviceName: string) { + const containers = await docker.listContainers({ + filters: { + label: [ + `com.docker.compose.service=${serviceName}`, + `com.docker.compose.project=${DEV_ENVIRONMENT.projectNamespace}` + ], + status: ['running'] + } + }); + return containers.length > 0; +} +/** + * Check if Tinybird is running. + * Checks for tinybird-local service in ghost-dev compose project. + */ +export async function isTinybirdAvailable(): Promise { + const docker = new Docker(); + const tinybirdAvailable = await isServiceAvailable(docker, TINYBIRD.LOCAL_HOST); + debug(`Tinybird availability for compose project ${DEV_ENVIRONMENT.projectNamespace}:`, tinybirdAvailable); + return tinybirdAvailable; +} diff --git a/e2e/helpers/pages/base-page.ts b/e2e/helpers/pages/base-page.ts new file mode 100644 index 0000000..c00b1db --- /dev/null +++ b/e2e/helpers/pages/base-page.ts @@ -0,0 +1,46 @@ +import {PageHttpLogger} from './page-http-logger'; +import {appConfig} from '@/helpers/utils/app-config'; +import type {Locator, Page, Response} from '@playwright/test'; + +export interface pageGotoOptions { + referer?: string; + timeout?: number; + waitUntil?: 'load' | 'domcontentloaded'|'networkidle'|'commit'; +} + +export class BasePage { + private logger?: PageHttpLogger; + private readonly debugLogs = appConfig.debugLogs; + + public pageUrl: string = ''; + protected readonly page: Page; + public readonly body: Locator; + + constructor(page: Page, pageUrl: string = '') { + this.page = page; + this.pageUrl = pageUrl; + this.body = page.locator('body'); + + if (this.isDebugEnabled()) { + this.logger = new PageHttpLogger(page); + this.logger.setup(); + } + } + + async refresh() { + await this.page.reload(); + } + + async goto(url?: string, options?: pageGotoOptions): Promise { + const urlToVisit = url || this.pageUrl; + return await this.page.goto(urlToVisit, options); + } + + async pressKey(key: string) { + await this.page.keyboard.press(key); + } + + private isDebugEnabled(): boolean { + return this.debugLogs; + } +} diff --git a/e2e/helpers/pages/index.ts b/e2e/helpers/pages/index.ts new file mode 100644 index 0000000..ade888a --- /dev/null +++ b/e2e/helpers/pages/index.ts @@ -0,0 +1,5 @@ +export * from './base-page'; +export * from './admin'; +export * from './portal'; +export * from './public'; +export * from './stripe'; diff --git a/e2e/helpers/pages/page-http-logger.ts b/e2e/helpers/pages/page-http-logger.ts new file mode 100644 index 0000000..07690cf --- /dev/null +++ b/e2e/helpers/pages/page-http-logger.ts @@ -0,0 +1,41 @@ +import {Page, Request, Response} from '@playwright/test'; + +export class PageHttpLogger { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + public setup() { + this.page.on('response', this.onResponse); + this.page.on('requestfailed', this.onRequestFailed); + this.page.on('pageerror', this.onPageError); + } + + public destroy() { + this.page.off('response', this.onResponse); + this.page.off('requestfailed', this.onRequestFailed); + this.page.off('pageerror', this.onPageError); + } + + private onResponse = (response: Response) => { + if (response.status() >= 400) { + this.logError(`ERROR - HTTP: ${response.status()} ${response.url()}`); + } + }; + + private onRequestFailed = (request: Request) => { + this.logError(`ERROR - NETWORK: ${request.method()} ${request.url()} - ${request.failure()?.errorText}`); + }; + + private onPageError = (error: Error) => { + this.logError(`ERROR - JS: ${error.message}`); + }; + + private logError = (message: string) => { + const timestamp = new Date().toISOString(); + // eslint-disable-next-line no-console + console.error(`[${timestamp}] ${message}`); + }; +} diff --git a/e2e/helpers/playwright/fixture.ts b/e2e/helpers/playwright/fixture.ts new file mode 100644 index 0000000..0fa3601 --- /dev/null +++ b/e2e/helpers/playwright/fixture.ts @@ -0,0 +1,510 @@ +import baseDebug from '@tryghost/debug'; +import {AnalyticsOverviewPage} from '@/helpers/pages'; +import {Browser, BrowserContext, Page, TestInfo, test as base} from '@playwright/test'; +import {EmailClient, MailPit} from '@/helpers/services/email/mail-pit'; +import {FakeMailgunServer, MailgunTestService} from '@/helpers/services/mailgun'; +import {FakeStripeServer, StripeTestService, WebhookClient} from '@/helpers/services/stripe'; +import {GhostInstance, getEnvironmentManager} from '@/helpers/environment'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {faker} from '@faker-js/faker'; +import {loginToGetAuthenticatedSession} from '@/helpers/playwright/flows/sign-in'; +import {setupUser} from '@/helpers/utils'; + +const debug = baseDebug('e2e:ghost-fixture'); +const STRIPE_SECRET_KEY = 'sk_test_e2eTestKey'; +const STRIPE_PUBLISHABLE_KEY = 'pk_test_e2eTestKey'; + +type ResolvedIsolation = 'per-file' | 'per-test'; +type LabsFlags = Record; + +/** + * The subset of fixture options that defines whether a per-file environment can + * be reused for the next test in the same file. + * + * Any new fixture option that changes persistent Ghost state or boot-time config + * must make an explicit choice: + * - include it here so it participates in environment reuse, or + * - force per-test isolation instead of participating in per-file reuse. + */ +interface EnvironmentIdentity { + config?: GhostConfig; + labs?: LabsFlags; +} + +interface PerFileInstanceCache { + suiteKey: string; + environmentSignature: string; + instance: GhostInstance; +} + +interface PerFileAuthenticatedSessionCache { + ghostAccountOwner: User; + storageState: Awaited>; +} + +interface TestEnvironmentContext { + holder: GhostInstance; + resolvedIsolation: ResolvedIsolation; + cycle: () => Promise; + getResetEnvironmentBlocker: () => string | null; + markResetEnvironmentBlocker: (fixtureName: string) => void; +} + +interface InternalFixtures { + _testEnvironmentContext: TestEnvironmentContext; +} + +interface WorkerFixtures { + _cleanupPerFileInstance: void; +} + +let cachedPerFileInstance: PerFileInstanceCache | null = null; +let cachedPerFileGhostAccountOwner: User | null = null; +let cachedPerFileAuthenticatedSession: PerFileAuthenticatedSessionCache | null = null; + +export interface User { + name: string; + email: string; + password: string; +} + +export interface GhostConfig { + hostSettings__billing__enabled?: string; + hostSettings__billing__url?: string; + hostSettings__forceUpgrade?: string; + hostSettings__limits__customIntegrations__disabled?: string; + hostSettings__limits__customIntegrations__error?: string; +} + +export interface GhostInstanceFixture { + ghostInstance: GhostInstance; + // Opt a file into per-test isolation without relying on Playwright-wide fullyParallel. + isolation?: 'per-test'; + resolvedIsolation: ResolvedIsolation; + // Hook-only escape hatch for per-file mode before stateful fixtures are resolved. + resetEnvironment: () => Promise; + // Participates in per-file environment identity. + labs?: LabsFlags; + // Participates in per-file environment identity. + config?: GhostConfig; + // Forces per-test isolation because Ghost boots against a per-test fake Stripe server. + stripeEnabled?: boolean; + stripeServer?: FakeStripeServer; + stripe?: StripeTestService; + mailgunEnabled?: boolean; + mailgunServer?: FakeMailgunServer; + mailgun?: MailgunTestService; + emailClient: EmailClient; + ghostAccountOwner: User; + pageWithAuthenticatedUser: { + page: Page; + context: BrowserContext; + ghostAccountOwner: User + }; +} + +function getStableObjectSignature(values?: T): string { + return JSON.stringify( + Object.fromEntries( + Object.entries(values ?? {}) + .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)) + ) + ); +} + +function getEnvironmentSignature(identity: EnvironmentIdentity): string { + return JSON.stringify({ + config: getStableObjectSignature(identity.config), + labs: getStableObjectSignature(identity.labs) + }); +} + +function getSuiteKey(testInfo: TestInfo): string { + return `${testInfo.project.name}:${testInfo.file}`; +} + +function getResolvedIsolation(testInfo: TestInfo, isolation?: 'per-test'): ResolvedIsolation { + if (testInfo.config.fullyParallel || isolation === 'per-test') { + return 'per-test'; + } + + return 'per-file'; +} + +async function setupNewAuthenticatedPage(browser: Browser, baseURL: string, ghostAccountOwner: User) { + debug('Setting up authenticated page for Ghost instance:', baseURL); + + // Create browser context with correct baseURL and extra HTTP headers + const context = await browser.newContext({ + baseURL: baseURL, + extraHTTPHeaders: { + Origin: baseURL + } + }); + const page = await context.newPage(); + + await loginToGetAuthenticatedSession(page, ghostAccountOwner.email, ghostAccountOwner.password); + debug('Authentication completed for Ghost instance'); + + return {page, context, ghostAccountOwner}; +} + +async function setupAuthenticatedPageFromStorageState(browser: Browser, baseURL: string, authenticatedSession: PerFileAuthenticatedSessionCache) { + debug('Reusing authenticated storage state for Ghost instance:', baseURL); + + const context = await browser.newContext({ + baseURL: baseURL, + extraHTTPHeaders: { + Origin: baseURL + }, + storageState: authenticatedSession.storageState + }); + const page = await context.newPage(); + await page.goto('/ghost/#/'); + + const analyticsPage = new AnalyticsOverviewPage(page); + const billingIframe = page.getByTitle('Billing'); + await Promise.race([ + analyticsPage.header.waitFor({state: 'visible'}), + billingIframe.waitFor({state: 'visible'}) + ]); + + return { + page, + context, + ghostAccountOwner: authenticatedSession.ghostAccountOwner + }; +} + +/** + * Playwright fixture that provides a unique Ghost instance for each test + * Each instance gets its own database, runs on a unique port, and includes authentication + * + * Uses the unified E2E environment manager: + * - Dev mode (default): Worker-scoped containers with per-test database cloning + * - Build mode: Same isolation model, but Ghost runs from a prebuilt image + * + * Optionally allows setting labs flags via test.use({labs: {featureName: true}}) + * and Stripe connection via test.use({stripeEnabled: true}) + */ +export const test = base.extend({ + _cleanupPerFileInstance: [async ({}, use) => { + await use(); + + if (!cachedPerFileInstance) { + return; + } + + const environmentManager = await getEnvironmentManager(); + await environmentManager.perTestTeardown(cachedPerFileInstance.instance); + cachedPerFileInstance = null; + cachedPerFileGhostAccountOwner = null; + cachedPerFileAuthenticatedSession = null; + }, { + scope: 'worker', + auto: true + }], + + _testEnvironmentContext: async ({config, isolation, labs, stripeEnabled, stripeServer, mailgunEnabled, mailgunServer}, use, testInfo: TestInfo) => { + const environmentManager = await getEnvironmentManager(); + const requestedIsolation = getResolvedIsolation(testInfo, isolation); + // Stripe-enabled tests boot Ghost against a per-test fake Stripe server, + // so they cannot safely participate in per-file environment reuse. + const resolvedIsolation = stripeEnabled ? 'per-test' : requestedIsolation; + const suiteKey = getSuiteKey(testInfo); + const stripeConfig = stripeEnabled && stripeServer ? { + STRIPE_API_HOST: 'host.docker.internal', + STRIPE_API_PORT: String(stripeServer.port), + STRIPE_API_PROTOCOL: 'http' + } : {}; + const mailgunConfig = mailgunEnabled && mailgunServer ? { + bulkEmail__mailgun__apiKey: 'fake-mailgun-api-key', + bulkEmail__mailgun__domain: 'fake.mailgun.test', + bulkEmail__mailgun__baseUrl: `http://host.docker.internal:${mailgunServer.port}/v3` + } : {}; + const mergedConfig = {...(config || {}), ...stripeConfig, ...mailgunConfig}; + const stripe = stripeServer ? { + secretKey: STRIPE_SECRET_KEY, + publishableKey: STRIPE_PUBLISHABLE_KEY + } : undefined; + const environmentIdentity: EnvironmentIdentity = { + config: mergedConfig, + labs + }; + const environmentSignature = getEnvironmentSignature(environmentIdentity); + const resetEnvironmentGuard = { + blocker: null as string | null + }; + + if (resolvedIsolation === 'per-test') { + const perTestInstance = await environmentManager.perTestSetup({ + config: mergedConfig, + stripe + }); + const previousPerFileInstance = cachedPerFileInstance?.instance; + cachedPerFileInstance = null; + cachedPerFileGhostAccountOwner = null; + cachedPerFileAuthenticatedSession = null; + + if (previousPerFileInstance) { + await environmentManager.perTestTeardown(previousPerFileInstance); + } + + await use({ + holder: perTestInstance, + resolvedIsolation, + cycle: async () => { + debug('resetEnvironment() is a no-op in per-test isolation mode'); + }, + getResetEnvironmentBlocker: () => resetEnvironmentGuard.blocker, + markResetEnvironmentBlocker: (fixtureName: string) => { + resetEnvironmentGuard.blocker ??= fixtureName; + } + }); + + await environmentManager.perTestTeardown(perTestInstance); + return; + } + + const mustRecyclePerFileInstance = !cachedPerFileInstance || + cachedPerFileInstance.suiteKey !== suiteKey || + cachedPerFileInstance.environmentSignature !== environmentSignature; + + if (mustRecyclePerFileInstance) { + const previousPerFileInstance = cachedPerFileInstance?.instance; + const nextPerFileInstance = await environmentManager.perTestSetup({ + config: mergedConfig, + stripe + }); + cachedPerFileInstance = { + suiteKey, + environmentSignature, + instance: nextPerFileInstance + }; + cachedPerFileGhostAccountOwner = null; + cachedPerFileAuthenticatedSession = null; + + if (previousPerFileInstance) { + await environmentManager.perTestTeardown(previousPerFileInstance); + } + } + + const activePerFileInstance = cachedPerFileInstance; + if (!activePerFileInstance) { + throw new Error('[e2e fixture] Failed to initialize per-file Ghost instance.'); + } + + const holder = {...activePerFileInstance.instance}; + const cycle = async () => { + const previousInstance = cachedPerFileInstance?.instance; + const nextInstance = await environmentManager.perTestSetup({ + config: mergedConfig, + stripe + }); + + if (previousInstance) { + await environmentManager.perTestTeardown(previousInstance); + } + + cachedPerFileInstance = { + suiteKey, + environmentSignature, + instance: nextInstance + }; + cachedPerFileGhostAccountOwner = null; + cachedPerFileAuthenticatedSession = null; + + Object.assign(holder, nextInstance); + }; + + await use({ + holder, + resolvedIsolation, + cycle, + getResetEnvironmentBlocker: () => resetEnvironmentGuard.blocker, + markResetEnvironmentBlocker: (fixtureName: string) => { + resetEnvironmentGuard.blocker ??= fixtureName; + } + }); + }, + + // Define options that can be set per test or describe block + config: [undefined, {option: true}], + isolation: [undefined, {option: true}], + labs: [undefined, {option: true}], + stripeEnabled: [false, {option: true}], + mailgunEnabled: [false, {option: true}], + + stripeServer: async ({stripeEnabled}, use) => { + if (!stripeEnabled) { + await use(undefined); + return; + } + + const server = new FakeStripeServer(); + await server.start(); + debug('Fake Stripe server started on port', server.port); + + await use(server); + + await server.stop(); + debug('Fake Stripe server stopped'); + }, + + mailgunServer: async ({mailgunEnabled}, use) => { + if (!mailgunEnabled) { + await use(undefined); + return; + } + + const server = new FakeMailgunServer(); + await server.start(); + debug('Fake Mailgun server started on port', server.port); + + await use(server); + + await server.stop(); + debug('Fake Mailgun server stopped'); + }, + + mailgun: async ({mailgunEnabled, mailgunServer}, use) => { + if (!mailgunEnabled || !mailgunServer) { + await use(undefined); + return; + } + + const service = new MailgunTestService(mailgunServer); + await use(service); + }, + + emailClient: async ({}, use) => { + await use(new MailPit()); + }, + + ghostInstance: async ({_testEnvironmentContext}, use, testInfo: TestInfo) => { + debug('Using Ghost instance for test:', { + testTitle: testInfo.title, + resolvedIsolation: _testEnvironmentContext.resolvedIsolation, + ..._testEnvironmentContext.holder + }); + await use(_testEnvironmentContext.holder); + }, + + resolvedIsolation: async ({_testEnvironmentContext}, use) => { + await use(_testEnvironmentContext.resolvedIsolation); + }, + + resetEnvironment: async ({_testEnvironmentContext}, use) => { + await use(async () => { + if (_testEnvironmentContext.resolvedIsolation === 'per-test') { + debug('resetEnvironment() is a no-op in per-test isolation mode'); + return; + } + + // Only support resetEnvironment() before stateful fixtures such as the + // baseURL, authenticated user session, or page have been materialized. + const blocker = _testEnvironmentContext.getResetEnvironmentBlocker(); + if (blocker) { + throw new Error( + `[e2e fixture] resetEnvironment() must be called before resolving ` + + `"${blocker}". Use it in a beforeEach hook that only depends on ` + + 'resetEnvironment and fixtures that remain valid after a recycle.' + ); + } + + await _testEnvironmentContext.cycle(); + }); + }, + + stripe: async ({stripeEnabled, baseURL, stripeServer}, use) => { + if (!stripeEnabled || !baseURL || !stripeServer) { + await use(undefined); + return; + } + + const webhookClient = new WebhookClient(baseURL); + const service = new StripeTestService(stripeServer, webhookClient); + await use(service); + }, + + baseURL: async ({ghostInstance, _testEnvironmentContext}, use) => { + _testEnvironmentContext.markResetEnvironmentBlocker('baseURL'); + await use(ghostInstance.baseUrl); + }, + + // Create user credentials only (no authentication) + ghostAccountOwner: async ({ghostInstance, _testEnvironmentContext}, use) => { + if (!ghostInstance.baseUrl) { + throw new Error('baseURL is not defined'); + } + + _testEnvironmentContext.markResetEnvironmentBlocker('ghostAccountOwner'); + + if (_testEnvironmentContext.resolvedIsolation === 'per-file' && cachedPerFileGhostAccountOwner) { + await use(cachedPerFileGhostAccountOwner); + return; + } + + // Create user in this Ghost instance + const ghostAccountOwner: User = { + name: 'Test User', + email: `test${faker.string.uuid()}@ghost.org`, + password: 'test@123@test' + }; + await setupUser(ghostInstance.baseUrl, ghostAccountOwner); + + if (_testEnvironmentContext.resolvedIsolation === 'per-file') { + cachedPerFileGhostAccountOwner = ghostAccountOwner; + } + + await use(ghostAccountOwner); + }, + + // Intermediate fixture that sets up the page and returns all setup data + pageWithAuthenticatedUser: async ({browser, ghostInstance, ghostAccountOwner, _testEnvironmentContext}, use) => { + if (!ghostInstance.baseUrl) { + throw new Error('baseURL is not defined'); + } + + _testEnvironmentContext.markResetEnvironmentBlocker('pageWithAuthenticatedUser'); + + const pageWithAuthenticatedUser = + _testEnvironmentContext.resolvedIsolation === 'per-file' && cachedPerFileAuthenticatedSession + ? await setupAuthenticatedPageFromStorageState(browser, ghostInstance.baseUrl, cachedPerFileAuthenticatedSession) + : await setupNewAuthenticatedPage(browser, ghostInstance.baseUrl, ghostAccountOwner); + + if (_testEnvironmentContext.resolvedIsolation === 'per-file' && !cachedPerFileAuthenticatedSession) { + cachedPerFileAuthenticatedSession = { + ghostAccountOwner: pageWithAuthenticatedUser.ghostAccountOwner, + storageState: await pageWithAuthenticatedUser.context.storageState() + }; + } + + await use(pageWithAuthenticatedUser); + await pageWithAuthenticatedUser.context.close(); + }, + + // Extract the page from pageWithAuthenticatedUser and apply labs/stripe settings + page: async ({pageWithAuthenticatedUser, labs, _testEnvironmentContext}, use) => { + _testEnvironmentContext.markResetEnvironmentBlocker('page'); + + const page = pageWithAuthenticatedUser.page; + + const labsFlagsSpecified = labs && Object.keys(labs).length > 0; + if (labsFlagsSpecified) { + const settingsService = new SettingsService(page.request); + debug('Updating labs settings:', labs); + await settingsService.updateLabsSettings(labs); + } + + const needsReload = labsFlagsSpecified; + if (needsReload) { + await page.reload({waitUntil: 'load'}); + debug('Settings applied and page reloaded'); + } + + await use(page); + } +}); + +export {expect} from '@playwright/test'; diff --git a/e2e/helpers/playwright/index.ts b/e2e/helpers/playwright/index.ts new file mode 100644 index 0000000..1f5b02b --- /dev/null +++ b/e2e/helpers/playwright/index.ts @@ -0,0 +1,3 @@ +export * from './fixture'; +export * from './with-isolated-page'; +export * from './flows'; diff --git a/e2e/helpers/playwright/isolation.ts b/e2e/helpers/playwright/isolation.ts new file mode 100644 index 0000000..808d706 --- /dev/null +++ b/e2e/helpers/playwright/isolation.ts @@ -0,0 +1,38 @@ +import {test} from './fixture'; + +/** + * Opts a test file into per-test isolation (one Ghost environment per test). + * + * By default, e2e tests use per-file isolation: all tests in a file share a + * single Ghost instance and database, which is significantly faster on CI. + * + * Call this at the root level of any test file that needs a fresh Ghost + * environment for every test — typically files where tests mutate shared + * state (members, billing, settings) in ways that would interfere with + * each other. + * + * Under the hood this does two things via standard Playwright APIs: + * 1. `test.describe.configure({mode: 'parallel'})` — tells Playwright to + * run the file's tests concurrently across workers. + * 2. `test.use({isolation: 'per-test'})` — tells our fixture layer to + * spin up a dedicated Ghost instance per test instead of reusing one. + * + * Keeping both calls together avoids mismatches (e.g. parallel mode without + * per-test isolation) and replaces the previous monkey-patching approach + * that intercepted test.describe.configure() and parsed stack traces to + * detect the caller file. This is intentionally two standard Playwright + * calls wrapped in a single helper — no runtime patching required. + * + * @example + * ```ts + * import {usePerTestIsolation} from '@/helpers/playwright/isolation'; + * + * usePerTestIsolation(); + * + * test.describe('Ghost Admin - Members', () => { ... }); + * ``` + */ +export function usePerTestIsolation() { + test.describe.configure({mode: 'parallel'}); + test.use({isolation: 'per-test'}); +} diff --git a/e2e/helpers/playwright/with-isolated-page.ts b/e2e/helpers/playwright/with-isolated-page.ts new file mode 100644 index 0000000..f7ffc72 --- /dev/null +++ b/e2e/helpers/playwright/with-isolated-page.ts @@ -0,0 +1,17 @@ +import {Browser, BrowserContext, Page} from '@playwright/test'; + +export async function withIsolatedPage( + browser: Browser, + opts: Parameters[0], + run: ({page, context}: {page: Page, context: BrowserContext}) => Promise +): Promise { + const context = await browser.newContext(opts); + const page = await context.newPage(); + try { + return await run({page, context}); + } finally { + await page.close(); + await context.close(); + } +} + diff --git a/e2e/helpers/services/fake-server.ts b/e2e/helpers/services/fake-server.ts new file mode 100644 index 0000000..dd3f83f --- /dev/null +++ b/e2e/helpers/services/fake-server.ts @@ -0,0 +1,56 @@ +import baseDebug from '@tryghost/debug'; +import express from 'express'; +import http from 'http'; + +export abstract class FakeServer { + private server: http.Server | null = null; + protected readonly app: express.Express = express(); + private _port: number; + protected readonly debug: (...args: unknown[]) => void; + + constructor(options: {port?: number; debugNamespace: string}) { + this._port = options.port ?? 0; + this.debug = baseDebug(options.debugNamespace); + this.app.use((req, _res, next) => { + this.debug(`${req.method} ${req.originalUrl}`); + next(); + }); + this.setupRoutes(); + } + + get port(): number { + return this._port; + } + + async start(): Promise { + return new Promise((resolve, reject) => { + this.server = this.app.listen(this._port, () => { + const address = this.server?.address(); + + if (!address || typeof address === 'string') { + reject(new Error(`${this.constructor.name} did not expose a TCP port`)); + return; + } + + this._port = address.port; + resolve(); + }); + this.server.on('error', reject); + }); + } + + async stop(): Promise { + return new Promise((resolve) => { + if (!this.server) { + resolve(); + return; + } + this.server.close(() => { + this.server = null; + resolve(); + }); + }); + } + + protected abstract setupRoutes(): void; +} diff --git a/e2e/helpers/utils/app-config.ts b/e2e/helpers/utils/app-config.ts new file mode 100644 index 0000000..600cc4d --- /dev/null +++ b/e2e/helpers/utils/app-config.ts @@ -0,0 +1,13 @@ +import dotenv from 'dotenv'; +// load environment variables from .env file +dotenv.config({quiet: true}); + +// Simple config object with just the values +export const appConfig = { + baseURL: process.env.GHOST_BASE_URL || 'http://localhost:2368', + + auth: { + storageFile: 'playwright/.auth/user.json' + }, + debugLogs: process.env.E2E_DEBUG_LOGS === 'true' || process.env.E2E_DEBUG_LOGS === '1' +}; diff --git a/e2e/helpers/utils/ensure-dir.ts b/e2e/helpers/utils/ensure-dir.ts new file mode 100644 index 0000000..825ddbc --- /dev/null +++ b/e2e/helpers/utils/ensure-dir.ts @@ -0,0 +1,17 @@ +import * as fs from 'fs'; +import logging from '@tryghost/logging'; + +/** Ensure the state directory exists. */ +export const ensureDir = (dirPath: string) => { + try { + fs.mkdirSync(dirPath, {recursive: true}); + } catch (error) { + // Log with structured context and rethrow preserving the original error as the cause + logging.error({ + message: 'Failed to ensure directory exists', + dirPath, + err: error + }); + throw new Error(`failed to ensure directory ${dirPath} exists`, {cause: error as Error}); + } +}; diff --git a/e2e/helpers/utils/index.ts b/e2e/helpers/utils/index.ts new file mode 100644 index 0000000..616f96f --- /dev/null +++ b/e2e/helpers/utils/index.ts @@ -0,0 +1,3 @@ +export * from './app-config'; +export * from './setup-user'; +export * from './ensure-dir'; diff --git a/e2e/helpers/utils/setup-user.ts b/e2e/helpers/utils/setup-user.ts new file mode 100644 index 0000000..7ebe454 --- /dev/null +++ b/e2e/helpers/utils/setup-user.ts @@ -0,0 +1,60 @@ +import baseDebug from '@tryghost/debug'; +import {User, UserFactory} from '@/data-factory'; + +const debug = baseDebug('e2e:helpers:utils:setup-user'); + +export class GhostUserSetup { + private readonly baseURL: string; + private readonly headers: Record; + private readonly setupAuthEndpoint = '/ghost/api/admin/authentication/setup'; + + constructor(baseURL: string) { + this.baseURL = baseURL; + this.headers = {'Content-Type': 'application/json'}; + } + + async setup(userOverrides: Partial = {}): Promise { + debug('setup-user called'); + if (await this.isSetupAlreadyCompleted()) { + debug('Ghost user setup is already completed.'); + return; + } + + const user = new UserFactory().build(userOverrides); + await this.createUser(user); + } + + private async isSetupAlreadyCompleted(): Promise { + const response = await this.makeRequest('GET'); + const data = await response.json(); + debug('Setup status response:', data); + return data.setup?.[0]?.status === true; + } + + private async createUser(user: User): Promise { + await this.makeRequest('POST', {setup: [user]}); + debug('Ghost user created successfully.'); + } + + private async makeRequest(method: 'GET' | 'POST', body?: unknown): Promise { + const options: RequestInit = {method, headers: this.headers}; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(`${this.baseURL}${this.setupAuthEndpoint}`, options); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Ghost setup ${method} failed (${response.status}): ${error}`); + } + + return response; + } +} + +export async function setupUser(baseGhostUrl: string, user: Partial = {}): Promise { + const ghostUserSetup = new GhostUserSetup(baseGhostUrl); + await ghostUserSetup.setup(user); +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..71763f7 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,53 @@ +{ + "name": "@tryghost/e2e", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/e2e", + "author": "Ghost Foundation", + "private": true, + "type": "module", + "scripts": { + "dev": "tsc --watch --preserveWatchOutput --noEmit", + "build": "pnpm test:types", + "build:ts": "tsc --noEmit", + "build:apps": "pnpm nx run-many --target=build --projects=@tryghost/portal,@tryghost/comments-ui,@tryghost/sodo-search,@tryghost/signup-form,@tryghost/announcement-bar", + "build:docker": "docker build -f Dockerfile.e2e --build-arg GHOST_IMAGE=${GHOST_E2E_BASE_IMAGE:?Set GHOST_E2E_BASE_IMAGE} -t ${GHOST_E2E_IMAGE:-ghost-e2e:local} ..", + "pretest": "test -n \"$CI\" || echo 'Tip: run pnpm dev or pnpm --filter @tryghost/e2e infra:up before running tests'", + "infra:up": "bash ./scripts/infra-up.sh", + "infra:down": "bash ./scripts/infra-down.sh", + "tinybird:sync": "node ./scripts/sync-tinybird-state.mjs", + "preflight:build": "bash ./scripts/prepare-ci-e2e-build-mode.sh", + "test": "bash ./scripts/run-playwright-host.sh playwright test --project=main", + "test:analytics": "bash ./scripts/run-playwright-host.sh playwright test --project=analytics", + "test:all": "bash ./scripts/run-playwright-host.sh playwright test --project=main --project=analytics", + "test:single": "bash ./scripts/run-playwright-host.sh playwright test --project=main -g", + "test:debug": "bash ./scripts/run-playwright-host.sh playwright test --project=main --headed --timeout=60000 -g", + "test:types": "tsc --noEmit", + "lint": "eslint . --cache" + }, + "files": [ + "build" + ], + "devDependencies": { + "@eslint/js": "catalog:", + "@faker-js/faker": "8.4.1", + "@playwright/test": "1.59.1", + "@tryghost/debug": "0.1.40", + "@tryghost/logging": "2.5.5", + "@types/dockerode": "3.3.47", + "@types/express": "4.17.25", + "busboy": "^1.6.0", + "c8": "10.1.3", + "dockerode": "4.0.10", + "dotenv": "17.3.1", + "eslint": "catalog:", + "eslint-plugin-no-relative-import-paths": "1.6.1", + "eslint-plugin-playwright": "2.10.1", + "express": "4.21.2", + "knex": "3.1.0", + "mysql2": "3.18.1", + "stripe": "8.222.0", + "ts-node": "10.9.2", + "typescript": "5.9.3", + "typescript-eslint": "8.58.0" + } +} diff --git a/e2e/playwright.config.mjs b/e2e/playwright.config.mjs new file mode 100644 index 0000000..ae7bf1d --- /dev/null +++ b/e2e/playwright.config.mjs @@ -0,0 +1,71 @@ +import dotenv from 'dotenv'; +import os from 'os'; +dotenv.config({quiet: true}); + +/* + * 1/3 of the number of CPU cores seems to strike a good balance. Each worker + * runs in its own process (1 core) and gets its own Ghost instance (1 core) + * while leaving some head room for DBs, frontend dev servers, etc. + * + * It's possible to use more workers, but then the total test time and flakiness + * goes up dramatically. + */ +const getWorkerCount = () => { + const cpuCount = os.cpus().length; + return Math.floor(cpuCount / 3) || 1; +}; + +/** @type {import('@playwright/test').PlaywrightTestConfig} */ +const config = { + timeout: process.env.CI ? 30 * 1000 : 30 * 1000, + expect: { + timeout: process.env.CI ? 10 * 1000 : 10 * 1000 + }, + retries: 0, // Retries open the door to flaky tests. If the test needs retries, it's not a good test or the app is broken. + maxFailures: process.argv.includes('--ui') ? 0 : 1, + workers: parseInt(process.env.TEST_WORKERS_COUNT, 10) || getWorkerCount(), + fullyParallel: false, + reporter: process.env.CI ? [['list', {printSteps: true}], ['blob']] : [['list', {printSteps: true}], ['html', {open: 'never'}]], + use: { + // Base URL will be set dynamically per test via fixture + baseURL: process.env.GHOST_BASE_URL || 'http://localhost:2368', + trace: 'retain-on-failure', + browserName: 'chromium' + }, + testDir: './', + testMatch: ['tests/**/*.test.{js,ts}'], + projects: [ + { + name: 'global-setup', + testMatch: /global\.setup\.ts/, + testDir: './tests', + teardown: 'global-teardown', + timeout: 60 * 1000 // 60 seconds for setup + }, + { + name: 'main', + testIgnore: ['**/*.setup.ts', '**/*.teardown.ts', 'analytics/**/*.test.ts'], + testDir: './tests', + use: { + viewport: {width: 1920, height: 1080} + }, + dependencies: ['global-setup'] + }, + { + name: 'analytics', + testDir: './tests', + testMatch: ['analytics/**/*.test.ts'], + use: { + viewport: {width: 1920, height: 1080} + }, + dependencies: ['global-setup'] + }, + { + name: 'global-teardown', + testMatch: /global\.teardown\.ts/, + testDir: './tests' + } + ] +}; + +export default config; diff --git a/e2e/scripts/dump-e2e-docker-logs.sh b/e2e/scripts/dump-e2e-docker-logs.sh new file mode 100755 index 0000000..37e113c --- /dev/null +++ b/e2e/scripts/dump-e2e-docker-logs.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "::group::docker ps -a" +docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' +echo "::endgroup::" + +dump_container_logs() { + local pattern="$1" + local label="$2" + local found=0 + + while IFS= read -r container_name; do + if [[ -z "$container_name" ]]; then + continue + fi + + found=1 + echo "::group::${label}: ${container_name}" + docker inspect "$container_name" --format 'State={{json .State}}' || true + docker logs --tail=500 "$container_name" || true + echo "::endgroup::" + done < <(docker ps -a --format '{{.Names}}' | grep -E "$pattern" || true) + + if [[ "$found" -eq 0 ]]; then + echo "No containers matched ${label} pattern: ${pattern}" + fi +} + +dump_container_logs '^ghost-e2e-worker-' 'Ghost worker' +dump_container_logs '^ghost-e2e-gateway-' 'E2E gateway' +dump_container_logs '^ghost-dev-(mysql|redis|mailpit|analytics|analytics-db|tinybird-local|tb-cli)$' 'E2E infra' diff --git a/e2e/scripts/infra-down.sh b/e2e/scripts/infra-down.sh new file mode 100755 index 0000000..9c2433c --- /dev/null +++ b/e2e/scripts/infra-down.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT" + +docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml stop \ + analytics tb-cli tinybird-local mailpit redis mysql diff --git a/e2e/scripts/infra-up.sh b/e2e/scripts/infra-up.sh new file mode 100755 index 0000000..54bdfda --- /dev/null +++ b/e2e/scripts/infra-up.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$SCRIPT_DIR/resolve-e2e-mode.sh" + +cd "$REPO_ROOT" + +MODE="$(resolve_e2e_mode)" +export GHOST_E2E_MODE="$MODE" + +if [[ "$MODE" != "build" ]]; then + DEV_COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME:-ghost-dev}" + GHOST_DEV_IMAGE="${DEV_COMPOSE_PROJECT}-ghost-dev" + GATEWAY_IMAGE="${DEV_COMPOSE_PROJECT}-ghost-dev-gateway" + + if ! docker image inspect "$GHOST_DEV_IMAGE" >/dev/null 2>&1 || ! docker image inspect "$GATEWAY_IMAGE" >/dev/null 2>&1; then + echo "Building missing dev images for E2E (${GHOST_DEV_IMAGE}, ${GATEWAY_IMAGE})..." + docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml build ghost-dev ghost-dev-gateway + fi +fi + +docker compose -f compose.dev.yaml -f compose.dev.analytics.yaml up -d --wait \ + mysql redis mailpit tinybird-local analytics diff --git a/e2e/scripts/load-playwright-container-env.sh b/e2e/scripts/load-playwright-container-env.sh new file mode 100644 index 0000000..a8af603 --- /dev/null +++ b/e2e/scripts/load-playwright-container-env.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + echo "This script must be sourced, not executed" >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$REPO_ROOT" + +PLAYWRIGHT_VERSION="$(node -p 'require("./e2e/package.json").devDependencies["@playwright/test"]')" +PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble" +WORKSPACE_PATH="${GITHUB_WORKSPACE:-$REPO_ROOT}" + +export SCRIPT_DIR +export REPO_ROOT +export PLAYWRIGHT_VERSION +export PLAYWRIGHT_IMAGE +export WORKSPACE_PATH diff --git a/e2e/scripts/prepare-ci-e2e-build-mode.sh b/e2e/scripts/prepare-ci-e2e-build-mode.sh new file mode 100755 index 0000000..383045f --- /dev/null +++ b/e2e/scripts/prepare-ci-e2e-build-mode.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/load-playwright-container-env.sh" +GATEWAY_IMAGE="${GHOST_E2E_GATEWAY_IMAGE:-caddy:2-alpine}" + +echo "Preparing E2E build-mode runtime" +echo "Playwright image: ${PLAYWRIGHT_IMAGE}" +echo "Gateway image: ${GATEWAY_IMAGE}" + +pids=() +labels=() + +run_bg() { + local label="$1" + shift + labels+=("$label") + ( + echo "[${label}] starting" + "$@" + echo "[${label}] done" + ) & + pids+=("$!") +} + +run_bg "pull-gateway-image" docker pull "$GATEWAY_IMAGE" +run_bg "pull-playwright-image" docker pull "$PLAYWRIGHT_IMAGE" +run_bg "start-infra" env GHOST_E2E_MODE=build bash "$REPO_ROOT/e2e/scripts/infra-up.sh" + +for i in "${!pids[@]}"; do + if ! wait "${pids[$i]}"; then + echo "[${labels[$i]}] failed" >&2 + exit 1 + fi +done + +node "$REPO_ROOT/e2e/scripts/sync-tinybird-state.mjs" diff --git a/e2e/scripts/prepare-ci-e2e-job.sh b/e2e/scripts/prepare-ci-e2e-job.sh new file mode 100644 index 0000000..468c2e6 --- /dev/null +++ b/e2e/scripts/prepare-ci-e2e-job.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +SKIP_IMAGE_BUILD="${GHOST_E2E_SKIP_IMAGE_BUILD:-false}" + +if [[ "$SKIP_IMAGE_BUILD" != "true" && -z "${GHOST_E2E_BASE_IMAGE:-}" ]]; then + echo "GHOST_E2E_BASE_IMAGE is required when building the E2E image in-job" >&2 + exit 1 +fi + +cd "$REPO_ROOT" + +echo "Preparing CI E2E job" +echo "E2E image: ${GHOST_E2E_IMAGE:-ghost-e2e:local}" +echo "Skip image build: ${SKIP_IMAGE_BUILD}" + +if [[ "$SKIP_IMAGE_BUILD" != "true" ]]; then + echo "Base image: ${GHOST_E2E_BASE_IMAGE}" +fi + +pids=() +labels=() + +run_bg() { + local label="$1" + shift + labels+=("$label") + ( + echo "[${label}] starting" + "$@" + echo "[${label}] done" + ) & + pids+=("$!") +} + +# Mostly IO-bound runtime prep (image pulls + infra startup + Tinybird sync) can +# overlap with the app/docker builds. +run_bg "runtime-preflight" bash "$REPO_ROOT/e2e/scripts/prepare-ci-e2e-build-mode.sh" + +if [[ "$SKIP_IMAGE_BUILD" == "true" ]]; then + echo "Using prebuilt E2E image; skipping app and Docker image build." +else + # Build the assets + E2E image layer while IO-heavy prep is running. + pnpm --filter @tryghost/e2e build:apps + pnpm --filter @tryghost/e2e build:docker +fi + +for i in "${!pids[@]}"; do + if ! wait "${pids[$i]}"; then + echo "[${labels[$i]}] failed" >&2 + exit 1 + fi +done diff --git a/e2e/scripts/resolve-e2e-mode.sh b/e2e/scripts/resolve-e2e-mode.sh new file mode 100644 index 0000000..fefedfd --- /dev/null +++ b/e2e/scripts/resolve-e2e-mode.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +LOCAL_ADMIN_DEV_SERVER_URL="${LOCAL_ADMIN_DEV_SERVER_URL:-http://127.0.0.1:5174}" + +resolve_e2e_mode() { + if [[ -n "${GHOST_E2E_MODE:-}" ]]; then + case "$GHOST_E2E_MODE" in + dev|build) + printf '%s' "$GHOST_E2E_MODE" + return + ;; + *) + echo "Invalid GHOST_E2E_MODE: '$GHOST_E2E_MODE'. Expected one of: dev, build." >&2 + return 1 + ;; + esac + fi + + if curl --silent --fail --max-time 1 "$LOCAL_ADMIN_DEV_SERVER_URL" >/dev/null 2>&1; then + printf 'dev' + return + fi + + printf 'build' +} diff --git a/e2e/scripts/run-playwright-container.sh b/e2e/scripts/run-playwright-container.sh new file mode 100755 index 0000000..5894b43 --- /dev/null +++ b/e2e/scripts/run-playwright-container.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SHARD_INDEX="${E2E_SHARD_INDEX:-}" +SHARD_TOTAL="${E2E_SHARD_TOTAL:-}" +RETRIES="${E2E_RETRIES:-2}" + +if [[ -z "$SHARD_INDEX" || -z "$SHARD_TOTAL" ]]; then + echo "Missing E2E_SHARD_INDEX or E2E_SHARD_TOTAL environment variables" >&2 + exit 1 +fi + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/load-playwright-container-env.sh" + +docker run --rm --network host --ipc host \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "${WORKSPACE_PATH}:${WORKSPACE_PATH}" \ + -w "${WORKSPACE_PATH}/e2e" \ + -e CI=true \ + -e TEST_WORKERS_COUNT="${TEST_WORKERS_COUNT:-1}" \ + -e COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-ghost-dev}" \ + -e GHOST_E2E_MODE="${GHOST_E2E_MODE:-build}" \ + -e GHOST_E2E_IMAGE="${GHOST_E2E_IMAGE:-ghost-e2e:local}" \ + -e GHOST_E2E_GATEWAY_IMAGE="${GHOST_E2E_GATEWAY_IMAGE:-caddy:2-alpine}" \ + "$PLAYWRIGHT_IMAGE" \ + bash -c "corepack enable && pnpm test:all --shard=${SHARD_INDEX}/${SHARD_TOTAL} --retries=${RETRIES}" diff --git a/e2e/scripts/run-playwright-host.sh b/e2e/scripts/run-playwright-host.sh new file mode 100755 index 0000000..b4e7e6b --- /dev/null +++ b/e2e/scripts/run-playwright-host.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$SCRIPT_DIR/resolve-e2e-mode.sh" + +cd "$REPO_ROOT" + +GHOST_E2E_MODE="$(resolve_e2e_mode)" +export GHOST_E2E_MODE + +if [[ "$GHOST_E2E_MODE" == "dev" ]]; then + echo "E2E mode: dev (detected admin dev server at $LOCAL_ADMIN_DEV_SERVER_URL)" +else + echo "E2E mode: build (admin dev server not detected at $LOCAL_ADMIN_DEV_SERVER_URL)" + echo " Tip: For local development, run 'pnpm dev' first — dev mode is faster and doesn't require a pre-built Docker image." +fi + +# Dev-mode E2E Ghost containers mount the local workspace package, which needs a +# built entrypoint before Ghost can require it during boot. +if [[ "$GHOST_E2E_MODE" == "dev" ]]; then + pnpm --filter @tryghost/parse-email-address build >/dev/null +fi + +if [[ "${CI:-}" != "true" ]]; then + node "$REPO_ROOT/e2e/scripts/sync-tinybird-state.mjs" +fi + +cd "$REPO_ROOT/e2e" +exec "$@" diff --git a/e2e/scripts/sync-tinybird-state.mjs b/e2e/scripts/sync-tinybird-state.mjs new file mode 100644 index 0000000..c679b63 --- /dev/null +++ b/e2e/scripts/sync-tinybird-state.mjs @@ -0,0 +1,116 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {execFileSync} from 'node:child_process'; +import {fileURLToPath} from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../..'); +const stateDir = path.resolve(repoRoot, 'e2e/data/state'); +const configPath = path.resolve(stateDir, 'tinybird.json'); + +const composeArgs = [ + 'compose', + '-f', path.resolve(repoRoot, 'compose.dev.yaml'), + '-f', path.resolve(repoRoot, 'compose.dev.analytics.yaml') +]; +const composeProject = process.env.COMPOSE_PROJECT_NAME || 'ghost-dev'; + +function log(message) { + process.stdout.write(`${message}\n`); +} + +function parseEnv(raw) { + const vars = {}; + + for (const line of raw.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const separatorIndex = trimmed.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + vars[trimmed.slice(0, separatorIndex).trim()] = trimmed.slice(separatorIndex + 1).trim(); + } + + return vars; +} + +function clearConfigIfPresent() { + if (fs.existsSync(configPath)) { + fs.rmSync(configPath, {force: true}); + log(`Removed stale Tinybird config at ${configPath}`); + } +} + +function runCompose(args) { + return execFileSync('docker', [...composeArgs, ...args], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); +} + +function isTinybirdRunning() { + const output = execFileSync('docker', [ + 'ps', + '--filter', `label=com.docker.compose.project=${composeProject}`, + '--filter', 'label=com.docker.compose.service=tinybird-local', + '--filter', 'status=running', + '--format', '{{.Names}}' + ], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }); + + return Boolean(output.trim()); +} + +function fetchConfigFromTbCli() { + return runCompose([ + 'run', + '--rm', + '-T', + 'tb-cli', + 'cat', + '/mnt/shared-config/.env.tinybird' + ]); +} + +function writeConfig(env) { + fs.mkdirSync(stateDir, {recursive: true}); + fs.writeFileSync(configPath, JSON.stringify({ + workspaceId: env.TINYBIRD_WORKSPACE_ID, + adminToken: env.TINYBIRD_ADMIN_TOKEN, + trackerToken: env.TINYBIRD_TRACKER_TOKEN + }, null, 2)); +} + +try { + if (!isTinybirdRunning()) { + clearConfigIfPresent(); + log(`Tinybird is not running for compose project ${composeProject}; skipping Tinybird state sync (non-analytics runs are allowed)`); + process.exit(0); + } + + const rawEnv = fetchConfigFromTbCli(); + const env = parseEnv(rawEnv); + + if (!env.TINYBIRD_WORKSPACE_ID || !env.TINYBIRD_ADMIN_TOKEN) { + clearConfigIfPresent(); + throw new Error('Tinybird is running but required config values are missing in /mnt/shared-config/.env.tinybird'); + } + + writeConfig(env); + log(`Wrote Tinybird config to ${configPath}`); +} catch (error) { + clearConfigIfPresent(); + const message = error instanceof Error ? error.message : String(error); + log(`Tinybird state sync failed: ${message}`); + process.exit(1); +} diff --git a/e2e/tests/admin/reset-password.test.ts b/e2e/tests/admin/reset-password.test.ts new file mode 100644 index 0000000..081a17b --- /dev/null +++ b/e2e/tests/admin/reset-password.test.ts @@ -0,0 +1,59 @@ +import {AnalyticsOverviewPage, LoginPage, PasswordResetPage, SettingsPage} from '@/admin-pages'; +import {EmailClient, MailPit} from '@/helpers/services/email/mail-pit'; +import {Page} from '@playwright/test'; +import {expect, test} from '@/helpers/playwright'; +import {extractPasswordResetLink} from '@/helpers/services/email/utils'; + +test.describe('Ghost Admin - Reset Password', () => { + const emailClient: EmailClient = new MailPit(); + + async function logout(page: Page) { + const loginPage = new LoginPage(page); + await loginPage.logout(); + } + + test('resets account owner password', async ({page, ghostAccountOwner}) => { + await logout(page); + const {email} = ghostAccountOwner; + const newPassword = 'test@lginSecure@123'; + + const loginPage = new LoginPage(page); + await loginPage.requestPasswordReset(ghostAccountOwner.email); + await expect.soft(loginPage.body).toContainText('An email with password reset instructions has been sent.'); + + const messages = await emailClient.search({subject: 'Reset Password', to: email}); + const latestMessage = await emailClient.getMessageDetailed(messages[0]); + const passwordResetUrl = extractPasswordResetLink(latestMessage); + await loginPage.goto(passwordResetUrl); + + const passwordResetPage = new PasswordResetPage(page); + await passwordResetPage.resetPassword(newPassword, newPassword); + + const analyticsPage = new AnalyticsOverviewPage(page); + await expect(analyticsPage.header).toBeVisible(); + }); + + test('resets account owner password when 2FA enabled', async ({page, ghostAccountOwner}) => { + const newPassword = 'test@lginSecure@123'; + + const settingsPage = new SettingsPage(page); + await settingsPage.staffSection.goto(); + await settingsPage.staffSection.enableRequireTwoFa(); + await logout(page); + + const loginPage = new LoginPage(page); + await loginPage.requestPasswordReset(ghostAccountOwner.email); + await expect.soft(loginPage.body).toContainText('An email with password reset instructions has been sent.'); + + const messages = await emailClient.search({subject: 'Reset Password', to: ghostAccountOwner.email}); + const latestMessage = await emailClient.getMessageDetailed(messages[0]); + const passwordResetUrl = extractPasswordResetLink(latestMessage); + await loginPage.goto(passwordResetUrl); + + const passwordResetPage = new PasswordResetPage(page); + await passwordResetPage.resetPassword(newPassword, newPassword); + + const analyticsPage = new AnalyticsOverviewPage(page); + await expect(analyticsPage.header).toBeVisible(); + }); +}); diff --git a/e2e/tests/admin/signin.test.ts b/e2e/tests/admin/signin.test.ts new file mode 100644 index 0000000..0233c08 --- /dev/null +++ b/e2e/tests/admin/signin.test.ts @@ -0,0 +1,38 @@ +import {LoginPage, PostsPage, TagsPage} from '@/admin-pages'; +import {Page} from '@playwright/test'; +import {expect, test} from '@/helpers/playwright'; + +test.describe('Ghost Admin - Signin Redirect', () => { + async function logout(page: Page) { + const loginPage = new LoginPage(page); + await loginPage.logout(); + } + + test('deep-linking to a React route while logged out redirects back after signin', async ({page, ghostAccountOwner}) => { + await logout(page); + + const tagsPage = new TagsPage(page); + await tagsPage.goto(); + + const loginPage = new LoginPage(page); + await expect(loginPage.signInButton).toBeVisible(); + + await loginPage.signIn(ghostAccountOwner.email, ghostAccountOwner.password); + + await tagsPage.waitForPageToFullyLoad(); + }); + + test('deep-linking to an Ember route while logged out redirects back after signin', async ({page, ghostAccountOwner}) => { + await logout(page); + + const postsPage = new PostsPage(page); + await postsPage.goto(); + + const loginPage = new LoginPage(page); + await expect(loginPage.signInButton).toBeVisible(); + + await loginPage.signIn(ghostAccountOwner.email, ghostAccountOwner.password); + + await postsPage.waitForPageToFullyLoad(); + }); +}); diff --git a/e2e/tests/admin/two-factor-auth.test.ts b/e2e/tests/admin/two-factor-auth.test.ts new file mode 100644 index 0000000..4414a17 --- /dev/null +++ b/e2e/tests/admin/two-factor-auth.test.ts @@ -0,0 +1,77 @@ +import {AnalyticsOverviewPage, LoginPage, LoginVerifyPage} from '@/admin-pages'; +import {EmailClient, EmailMessage, MailPit} from '@/helpers/services/email/mail-pit'; +import {expect, test, withIsolatedPage} from '@/helpers/playwright'; + +test.describe('Two-Factor authentication', () => { + const emailClient: EmailClient = new MailPit(); + + function parseCodeFromMessageSubject(message: EmailMessage) { + const subject = message.Subject; + const match = subject.match(/\d+/); + + if (!match) { + throw new Error(`No verification code found in subject: ${subject}`); + } + + return match[0]; + } + + test.beforeEach(async ({page}) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + }); + + test('authenticates with 2FA token', async ({browser, baseURL, ghostAccountOwner}) => { + await withIsolatedPage(browser, {baseURL}, async ({page: page}) => { + const {email, password} = ghostAccountOwner; + const adminLoginPage = new LoginPage(page); + await adminLoginPage.goto(); + await adminLoginPage.signIn(email, password); + + const messages = await emailClient.search({ + subject: 'verification code', + to: ghostAccountOwner.email + }); + const code = parseCodeFromMessageSubject(messages[0]); + + const verifyPage = new LoginVerifyPage(page); + await verifyPage.twoFactorTokenField.fill(code); + await verifyPage.twoFactorVerifyButton.click(); + + const adminAnalyticsPage = new AnalyticsOverviewPage(page); + await expect(adminAnalyticsPage.header).toBeVisible(); + }); + }); + + test('authenticates with 2FA token that was resent', async ({browser, baseURL,ghostAccountOwner}) => { + await withIsolatedPage(browser, {baseURL}, async ({page: page}) => { + const {email, password} = ghostAccountOwner; + const adminLoginPage = new LoginPage(page); + await adminLoginPage.goto(); + await adminLoginPage.signIn(email, password); + + let messages = await emailClient.search({ + subject: 'verification code', + to: ghostAccountOwner.email + }); + expect(messages.length).toBe(1); + + const verifyPage = new LoginVerifyPage(page); + await verifyPage.resendTwoFactorCodeButton.click(); + + messages = await emailClient.search({ + subject: 'verification code', + to: ghostAccountOwner.email + }, {numberOfMessages: 2}); + + expect(messages.length).toBe(2); + + const code = parseCodeFromMessageSubject(messages[0]); + await verifyPage.twoFactorTokenField.fill(code); + await verifyPage.twoFactorVerifyButton.click(); + + const adminAnalyticsPage = new AnalyticsOverviewPage(page); + await expect(adminAnalyticsPage.header).toBeVisible(); + }); + }); +}); diff --git a/e2e/tests/admin/whats-new.test.ts b/e2e/tests/admin/whats-new.test.ts new file mode 100644 index 0000000..218b138 --- /dev/null +++ b/e2e/tests/admin/whats-new.test.ts @@ -0,0 +1,301 @@ +import {WhatsNewBanner, WhatsNewMenu} from '@/admin-pages'; +import {expect, test} from '@/helpers/playwright/fixture'; +import type {Page} from '@playwright/test'; + +// Local type definition matching the API response format +type RawChangelogEntry = { + slug: string; + title: string; + custom_excerpt: string; + published_at: string; + url: string; + featured: string; + feature_image?: string; + html?: string; +}; + +function daysAgo(days: number): Date { + const date = new Date(); + date.setDate(date.getDate() - days); + return date; +} + +function daysFromNow(days: number): Date { + const date = new Date(); + date.setDate(date.getDate() + days); + return date; +} + +function createEntry(publishedAt: Date, options: { + featured?: boolean; + title?: string; + excerpt?: string; + feature_image?: string; +} = {}): RawChangelogEntry { + const title = options.title ?? 'Test Update'; + const slug = title.toLowerCase().replace(/\s+/g, '-'); + return { + slug, + title, + custom_excerpt: options.excerpt ?? 'Test feature', + published_at: publishedAt.toISOString(), + url: `https://ghost.org/changelog/${slug}`, + featured: (options.featured ?? false) ? 'true' : 'false', + ...(options.feature_image && {feature_image: options.feature_image}) + }; +} + +async function mockChangelog(page: Page, entries: RawChangelogEntry[]): Promise { + await page.route('https://ghost.org/changelog.json', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + posts: entries, + changelogUrl: 'https://ghost.org/changelog/' + }) + }); + }); +} + +test.describe('Ghost Admin - What\'s New', () => { + test.describe('banner notification', () => { + test('shows banner for new entries the user has not seen', async ({page}) => { + await mockChangelog(page, [ + createEntry(daysFromNow(1), { + title: 'New Update', + excerpt: 'This is an exciting new feature' + }) + ]); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + await banner.waitForBanner(); + + await expect(banner.container).toBeVisible(); + await expect(banner.title).toHaveText('New Update'); + await expect(banner.excerpt).toHaveText('This is an exciting new feature'); + }); + + test('does not show banner for entries from before user joined', async ({page}) => { + await mockChangelog(page, [createEntry(daysAgo(30))]); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + + await expect(banner.container).toBeHidden(); + }); + + test('does not show banner when there are no entries', async ({page}) => { + await mockChangelog(page, []); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + + await expect(banner.container).toBeHidden(); + }); + + test.describe('dismissal behavior', () => { + test('hides banner immediately when close button is clicked', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + await banner.waitForBanner(); + + await expect(banner.container).toBeVisible(); + + await banner.dismiss(); + + await expect(banner.container).toBeHidden(); + }); + + test('hides banner immediately when link is clicked', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + await banner.waitForBanner(); + + await expect(banner.container).toBeVisible(); + + await banner.clickLinkAndClosePopup(); + + await expect(banner.container).toBeHidden(); + }); + + test('hides banner immediately when modal is opened', async ({page}) => { + await mockChangelog(page, [ + createEntry(daysFromNow(1), {feature_image: 'https://ghost.org/image1.jpg'}), + createEntry(daysAgo(5)) + ]); + + const banner = new WhatsNewBanner(page); + const menu = new WhatsNewMenu(page); + + await banner.goto(); + await banner.waitForBanner(); + + await expect(banner.container).toBeVisible(); + + const modal = await menu.openWhatsNewModal(); + await modal.close(); + + await expect(banner.container).toBeHidden(); + }); + + test('banner remains hidden after reload when dismissed', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const banner = new WhatsNewBanner(page); + await banner.goto(); + await banner.waitForBanner(); + + await banner.dismiss(); + + await banner.goto(); + await expect(banner.container).toBeHidden(); + }); + + test('banner reappears when a new entry is published after dismissal', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const banner = new WhatsNewBanner(page); + + await banner.goto(); + await banner.waitForBanner(); + await banner.dismiss(); + + await banner.goto(); + await expect(banner.container).toBeHidden(); + + await mockChangelog(page, [ + createEntry(daysFromNow(2), { + title: 'Second Update' + }) + ]); + + await banner.goto(); + await banner.waitForBanner(); + + await expect(banner.container).toBeVisible(); + await expect(banner.title).toHaveText('Second Update'); + }); + }); + }); + + test.describe('modal', () => { + test('shows modal with all entries when opened from user menu', async ({page}) => { + await mockChangelog(page, [ + createEntry(daysFromNow(1), { + title: 'Latest Update', + excerpt: 'Latest feature', + feature_image: 'https://ghost.org/image1.jpg' + }), + createEntry(daysAgo(5), { + title: 'Previous Update', + excerpt: 'Previous feature' + }) + ]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + const modal = await menu.openWhatsNewModal(); + + await expect(modal.modal).toBeVisible(); + await expect(modal.title).toBeVisible(); + + const entries = await modal.getEntries(); + expect(entries.length).toBe(2); + + expect(entries[0].title).toBe('Latest Update'); + expect(entries[0].excerpt).toBe('Latest feature'); + expect(entries[0].hasImage).toBe(true); + + expect(entries[1].title).toBe('Previous Update'); + expect(entries[1].excerpt).toBe('Previous feature'); + }); + }); + + test.describe('badge indicators', () => { + test('shows badge for new non-featured entries the user has not seen', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + await expect(menu.avatarBadge).toBeVisible(); + }); + + test('shows badge in user menu when there are new entries', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + await menu.openUserMenu(); + + await expect(menu.menuBadge).toBeVisible(); + }); + + test('does not show badges for entries from before user joined', async ({page}) => { + await mockChangelog(page, [createEntry(daysAgo(30))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + await expect(menu.avatarBadge).toBeHidden(); + + await menu.openUserMenu(); + await expect(menu.menuBadge).toBeHidden(); + }); + + test.describe('dismissal behavior', () => { + test('hides badges immediately when What\'s new modal is opened', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + await expect(menu.avatarBadge).toBeVisible(); + + const modal = await menu.openWhatsNewModal(); + await modal.close(); + + await expect(menu.avatarBadge).toBeHidden(); + }); + + test('badges remain hidden after reload when What\'s new has been viewed', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + const modal = await menu.openWhatsNewModal(); + await modal.close(); + + await menu.goto(); + await expect(menu.avatarBadge).toBeHidden(); + }); + + test('badges reappear when a new entry is published after viewing', async ({page}) => { + await mockChangelog(page, [createEntry(daysFromNow(1))]); + + const menu = new WhatsNewMenu(page); + await menu.goto(); + + const modal = await menu.openWhatsNewModal(); + await modal.close(); + + await menu.goto(); + await expect(menu.avatarBadge).toBeHidden(); + + await mockChangelog(page, [createEntry(daysFromNow(2))]); + + await menu.goto(); + + await expect(menu.avatarBadge).toBeVisible(); + }); + }); + }); +}); diff --git a/e2e/tests/global.setup.ts b/e2e/tests/global.setup.ts new file mode 100644 index 0000000..6a0f1a1 --- /dev/null +++ b/e2e/tests/global.setup.ts @@ -0,0 +1,10 @@ +import {getEnvironmentManager} from '@/helpers/environment'; +import {test as setup} from '@playwright/test'; + +const TIMEOUT = 2 * 60 * 1000; // 2 minutes + +setup('global environment setup', async () => { + setup.setTimeout(TIMEOUT); + const manager = await getEnvironmentManager(); + await manager.globalSetup(); +}); diff --git a/e2e/tests/global.teardown.ts b/e2e/tests/global.teardown.ts new file mode 100644 index 0000000..aff266a --- /dev/null +++ b/e2e/tests/global.teardown.ts @@ -0,0 +1,7 @@ +import {getEnvironmentManager} from '@/helpers/environment'; +import {test as teardown} from '@playwright/test'; + +teardown('global environment cleanup', async () => { + const manager = await getEnvironmentManager(); + await manager.globalTeardown(); +}); diff --git a/e2e/tests/portal/member-actions.test.ts b/e2e/tests/portal/member-actions.test.ts new file mode 100644 index 0000000..d4b5905 --- /dev/null +++ b/e2e/tests/portal/member-actions.test.ts @@ -0,0 +1,126 @@ +import {APIRequestContext, Page} from '@playwright/test'; +import {HomePage, MemberDetailsPage, MembersPage} from '@/helpers/pages'; +import {MemberFactory, createMemberFactory} from '@/data-factory'; +import {PortalAccountHomePage, PortalNewsletterManagementPage} from '@/portal-pages'; +import {expect, test} from '@/helpers/playwright'; +import {usePerTestIsolation} from '@/helpers/playwright/isolation'; + +usePerTestIsolation(); + +async function getNewsletterIds(request: APIRequestContext): Promise { + const response = await request.get('/ghost/api/admin/newsletters/?status=active&limit=all'); + const data = await response.json(); + return data.newsletters.map((n: {id: string}) => n.id); +} + +async function createNewsletter(request: APIRequestContext, name: string): Promise { + const response = await request.post('/ghost/api/admin/newsletters/', { + data: {newsletters: [{name}]} + }); + const data = await response.json(); + return data.newsletters[0].id; +} + +async function createSubscribedMember(request: APIRequestContext, memberFactory: MemberFactory) { + const newsletterIds = await getNewsletterIds(request); + const newsletters = newsletterIds.map(id => ({id})); + const member = await memberFactory.create({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newsletters: newsletters as any + }); + return member; +} + +async function impersonateMember(page: Page, memberName: string): Promise { + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.getMemberByName(memberName).click(); + + const memberDetailsPage = new MemberDetailsPage(page); + await memberDetailsPage.settingsSection.memberActionsButton.click(); + await memberDetailsPage.settingsSection.impersonateButton.click(); + + await expect(memberDetailsPage.magicLinkInput).not.toHaveValue(''); + const magicLink = await memberDetailsPage.magicLinkInput.inputValue(); + await memberDetailsPage.goto(magicLink); + + const homePage = new HomePage(page); + await homePage.waitUntilLoaded(); +} + +async function getMemberNewsletters(request: APIRequestContext, memberId: string): Promise<{id: string}[]> { + const response = await request.get(`/ghost/api/admin/members/${memberId}/`); + const data = await response.json(); + return data.members[0].newsletters; +} + +test.describe('Portal - Member Actions', () => { + let memberFactory: MemberFactory; + + test.beforeEach(async ({page}) => { + memberFactory = createMemberFactory(page.request); + }); + + test('can log out', async ({page}) => { + const member = await memberFactory.create(); + + await impersonateMember(page, member.name!); + + const homePage = new HomePage(page); + await homePage.openAccountPortal(); + + const accountHome = new PortalAccountHomePage(page); + await accountHome.signOut(); + + await homePage.openPortal(); + + await expect(accountHome.signinSwitchButton).toBeVisible(); + }); + + test('can unsubscribe from newsletter', async ({page}) => { + const member = await createSubscribedMember(page.request, memberFactory); + + await impersonateMember(page, member.name!); + + const homePage = new HomePage(page); + await homePage.openAccountPortal(); + + const accountHome = new PortalAccountHomePage(page); + await expect(accountHome.defaultNewsletterCheckbox).toBeChecked(); + await accountHome.defaultNewsletterToggle.click(); + await expect(accountHome.defaultNewsletterCheckbox).not.toBeChecked(); + + await expect(async () => { + const memberNewsletters = await getMemberNewsletters(page.request, member.id); + expect(memberNewsletters).toHaveLength(0); + }).toPass(); + }); + + test('can unsubscribe from all newsletters', async ({page}) => { + await createNewsletter(page.request, 'Second newsletter'); + + const member = await createSubscribedMember(page.request, memberFactory); + + await impersonateMember(page, member.name!); + + const homePage = new HomePage(page); + await homePage.openAccountPortal(); + + const accountHome = new PortalAccountHomePage(page); + await accountHome.manageNewslettersButton.click(); + + const newsletterManagement = new PortalNewsletterManagementPage(page); + await expect(newsletterManagement.newsletterToggles).toHaveCount(2); + await expect(newsletterManagement.newsletterToggleCheckbox(0)).toBeChecked(); + await expect(newsletterManagement.newsletterToggleCheckbox(1)).toBeChecked(); + + await newsletterManagement.unsubscribeFromAllButton.click(); + await expect(newsletterManagement.successNotification).toBeVisible(); + + await expect(newsletterManagement.newsletterToggleCheckbox(0)).not.toBeChecked(); + await expect(newsletterManagement.newsletterToggleCheckbox(1)).not.toBeChecked(); + + const memberNewsletters = await getMemberNewsletters(page.request, member.id); + expect(memberNewsletters).toHaveLength(0); + }); +}); diff --git a/e2e/tests/post-factory.test.ts b/e2e/tests/post-factory.test.ts new file mode 100644 index 0000000..cd30965 --- /dev/null +++ b/e2e/tests/post-factory.test.ts @@ -0,0 +1,56 @@ +import {PostPage} from '@/helpers/pages'; +import {PostsPage} from '@/admin-pages'; +import {createPostFactory} from '@/data-factory'; +import {expect, test} from '@/helpers/playwright'; +import type {PostFactory} from '@/data-factory'; + +test.describe('Post Factory API Integration', () => { + let postFactory: PostFactory; + + test.beforeEach(async ({page}) => { + postFactory = createPostFactory(page.request); + }); + + test('create a post and view it on the frontend', async ({page}) => { + const post = await postFactory.create({ + title: 'Test Post from Factory', + status: 'published' + }); + + expect(post.id).toBeTruthy(); + expect(post.slug).toBeTruthy(); + expect(post.status).toBe('published'); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await expect(postPage.postTitle).toContainText('Test Post from Factory'); + }); + + test('create a post visible in Ghost Admin', async ({page}) => { + const uniqueTitle = `Admin Test Post ${Date.now()}`; + const post = await postFactory.create({ + title: uniqueTitle, + status: 'published' + }); + + const postsPage = new PostsPage(page); + await postsPage.goto(); + await expect(postsPage.getPostByTitle(post.title)).toBeVisible(); + }); + + test('create draft post that is not accessible on frontend', async ({page}) => { + const draftPost = await postFactory.create({ + title: 'Draft Post from Factory', + status: 'draft' + }); + + expect(draftPost.status).toBe('draft'); + expect(draftPost.published_at).toBeNull(); + + // TODO: Replace this with a 404 page object + const response = await page.goto(`/${draftPost.slug}/`, { + waitUntil: 'domcontentloaded' + }); + expect(response?.status()).toBe(404); + }); +}); diff --git a/e2e/tests/public/comment-replies.test.ts b/e2e/tests/public/comment-replies.test.ts new file mode 100644 index 0000000..ebb8d2b --- /dev/null +++ b/e2e/tests/public/comment-replies.test.ts @@ -0,0 +1,134 @@ +import { + CommentFactory, + MemberFactory, + PostFactory, + TierFactory, + createFactories +} from '@/data-factory'; +import {PostPage} from '@/public-pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, signInAsMember, test} from '@/helpers/playwright'; + +test.describe('Ghost Public - Comments - Replies', () => { + let commentFactory: CommentFactory; + let postFactory: PostFactory; + let memberFactory: MemberFactory; + let tierFactory: TierFactory; + let settingsService: SettingsService; + + test.beforeEach(async ({page}) => { + ({postFactory, memberFactory, commentFactory, tierFactory} = createFactories(page.request)); + settingsService = new SettingsService(page.request); + }); + + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('all'); + }); + + test('reply to top comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + const comment = await commentFactory.create({ + html: 'Main comment', + post_id: post.id, + member_id: member.id + }); + + await commentFactory.create({ + html: 'Reply to main comment', + post_id: post.id, + member_id: paidMember.id, + parent_id: comment.id + }); + + await signInAsMember(page, paidMember); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + const postCommentsSection = postPage.commentsSection; + + await expect(postCommentsSection.comments).toHaveCount(2); + await expect(postCommentsSection.comments.first()).toContainText('Main comment'); + await expect(postCommentsSection.comments.last()).toContainText('Reply to main comment'); + + await postCommentsSection.replyToComment('Main comment', 'Reply to main comment 2'); + await expect(postCommentsSection.comments).toHaveCount(3); + await expect(postCommentsSection.comments.first()).toContainText('Main comment'); + await expect(postCommentsSection.comments.last()).toContainText('Reply to main comment 2'); + }); + + test('reply to reply comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + const comment = await commentFactory.create({ + html: 'Main comment', + post_id: post.id, + member_id: member.id + }); + + await commentFactory.create({ + html: 'Reply to main comment', + post_id: post.id, + member_id: paidMember.id, + parent_id: comment.id + }); + + await signInAsMember(page, paidMember); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + const postCommentsSection = postPage.commentsSection; + + await expect(postCommentsSection.comments).toHaveCount(2); + + await postCommentsSection.replyToComment('Reply to main comment', 'My reply'); + await expect(postCommentsSection.comments).toHaveCount(3); + await expect(postCommentsSection.comments.first()).toContainText('Main comment'); + await expect(postCommentsSection.comments.last()).toContainText('My reply'); + await expect(postCommentsSection.comments.last()).toContainText('Replied to: Reply to main comment'); + }); + + test('show replies and load more replies', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + + const comment = await commentFactory.create({ + html: 'Test comment 1', + post_id: post.id, + member_id: member.id + }); + + const replies = Array.from({length: 5}, (_, index) => { + return { + html: `reply ${index + 1} to comment 1`, + post_id: post.id, + member_id: member.id, + parent_id: comment.id + }; + }); + + await commentFactory.createMany(replies); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + const postCommentsSection = postPage.commentsSection; + + await expect(postCommentsSection.comments).toHaveCount(4); + await expect(postCommentsSection.comments.last()).toContainText('reply 3 to comment 1'); + await expect(postCommentsSection.showMoreRepliesButton).toBeVisible(); + await expect(postCommentsSection.showMoreRepliesButton).toContainText('Show 2 more replies'); + + await postCommentsSection.showMoreRepliesButton.click(); + await expect(postCommentsSection.comments.last()).toContainText('reply 5 to comment 1'); + await expect(postCommentsSection.comments).toHaveCount(6); + }); +}); diff --git a/e2e/tests/public/comments-manage.test.ts b/e2e/tests/public/comments-manage.test.ts new file mode 100644 index 0000000..4a8283d --- /dev/null +++ b/e2e/tests/public/comments-manage.test.ts @@ -0,0 +1,109 @@ +import { + CommentFactory, + MemberFactory, + PostFactory, + TierFactory, + createFactories +} from '@/data-factory'; +import {PostPage} from '@/public-pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, signInAsMember, test} from '@/helpers/playwright'; + +test.describe('Ghost Public - Comments - Manage', () => { + let commentFactory: CommentFactory; + let postFactory: PostFactory; + let memberFactory: MemberFactory; + let tierFactory: TierFactory; + let settingsService: SettingsService; + + test.beforeEach(async ({page}) => { + ({postFactory, memberFactory, commentFactory, tierFactory} = createFactories(page.request)); + + settingsService = new SettingsService(page.request); + }); + + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('all'); + }); + + test('no comment management buttons for non comment author', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + const anotherPaidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + await commentFactory.create({ + html: 'Comment to edit', + post_id: post.id, + member_id: paidMember.id + }); + + await signInAsMember(page, anotherPaidMember); + + const postPage = new PostPage(page); + const postCommentsSection = postPage.commentsSection; + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + + const { + editCommentButton, deleteCommentButton, hideCommentButton, showCommentButton + } = await postCommentsSection.getCommentActionButtons('Comment to edit'); + + await expect(editCommentButton).toBeHidden(); + await expect(deleteCommentButton).toBeHidden(); + await expect(hideCommentButton).toBeVisible(); + await expect(showCommentButton).toBeHidden(); + }); + + test('edit comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + await commentFactory.create({ + html: 'Comment to edit', + post_id: post.id, + member_id: paidMember.id + }); + + await signInAsMember(page, paidMember); + + const postPage = new PostPage(page); + const postCommentsSection = postPage.commentsSection; + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + + await postCommentsSection.editComment('Comment to edit', 'Updated comment'); + await expect(postCommentsSection.comments).toHaveCount(1); + await expect(postCommentsSection.comments.first()).toContainText('Updated comment'); + }); + + test('delete comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + await commentFactory.create({ + html: 'First comment', + post_id: post.id, + member_id: paidMember.id + }); + + await commentFactory.create({ + html: 'Comment to delete', + post_id: post.id, + member_id: paidMember.id + }); + + await signInAsMember(page, paidMember); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + const postCommentsSection = postPage.commentsSection; + + await postCommentsSection.deleteComment('Comment to delete'); + await expect(postCommentsSection.comments).toHaveCount(1); + await expect(postCommentsSection.comments.first()).toContainText('First comment'); + }); +}); diff --git a/e2e/tests/public/comments-permissions.test.ts b/e2e/tests/public/comments-permissions.test.ts new file mode 100644 index 0000000..960cfd7 --- /dev/null +++ b/e2e/tests/public/comments-permissions.test.ts @@ -0,0 +1,136 @@ +import {MemberFactory, PostFactory, TierFactory, createMemberFactory, createPostFactory, createTierFactory} from '@/data-factory'; +import {PostPage} from '@/public-pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, test} from '@/helpers/playwright'; +import {signInAsMember} from '@/helpers/playwright/flows/sign-in'; + +test.describe('Ghost Public - Comments - Permission', () => { + let postFactory: PostFactory; + let memberFactory: MemberFactory; + let tierFactory: TierFactory; + let settingsService: SettingsService; + + test.beforeEach(async ({page}) => { + postFactory = createPostFactory(page.request); + memberFactory = createMemberFactory(page.request); + tierFactory = createTierFactory(page.request); + settingsService = new SettingsService(page.request); + }); + + test.describe('comments enabled for all members', () => { + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('all'); + }); + + test('anonymous user - can not add a comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.commentsSection.waitForCommentsToLoad(); + + await expect(postPage.commentsSection.ctaBox).toBeVisible(); + await expect(postPage.commentsSection.signUpButton).toBeVisible(); + await expect(postPage.commentsSection.signInButton).toBeVisible(); + await expect(postPage.commentsSection.mainForm).toBeHidden(); + }); + + test('free member - can add a comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const freeMember = await memberFactory.create({status: 'free'}); + const commentTexts = ['Test comment by free member', 'Another Test comment by free member']; + + await signInAsMember(page, freeMember); + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.commentsSection.waitForCommentsToLoad(); + await postPage.commentsSection.addComment(commentTexts[0]); + await postPage.commentsSection.addComment(commentTexts[1]); + + await expect(postPage.commentsSection.mainForm).toBeVisible(); + await expect(postPage.commentsSection.ctaBox).toBeHidden(); + + // assert comment details + await expect(postPage.commentsSection.commentCountText).toHaveText('2 comments'); + await expect(postPage.commentsSection.comments).toHaveCount(2); + await expect(postPage.commentsSection.comments.first()).toContainText(commentTexts[1]); + await expect(postPage.commentsSection.comments.last()).toContainText(commentTexts[0]); + }); + + test('paid member - can add a comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + const commentText = 'This is a test comment from a paid member'; + + await signInAsMember(page, paidMember); + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForPostToLoad(); + await postPage.commentsSection.waitForCommentsToLoad(); + await postPage.commentsSection.addComment(commentText); + + await expect(postPage.commentsSection.mainForm).toBeVisible(); + await expect(postPage.commentsSection.ctaBox).toBeHidden(); + + // assert comment details + await expect(postPage.commentsSection.comments).toHaveCount(1); + await expect(postPage.commentsSection.comments.first()).toContainText(commentText); + }); + }); + + test.describe('comments enabled for paid members only', () => { + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('paid'); + }); + + test('free member - can not add a comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + + await signInAsMember(page, member); + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForPostToLoad(); + await postPage.commentsSection.waitForCommentsToLoad(); + + await expect(postPage.commentsSection.ctaBox).toBeVisible(); + await expect(postPage.commentsSection.mainForm).toBeHidden(); + }); + + test('paid member - can add a comment', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const member = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + const commentText = 'This is a test comment from a paid member'; + + await signInAsMember(page, member); + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForPostToLoad(); + await postPage.commentsSection.waitForCommentsToLoad(); + await postPage.commentsSection.addComment(commentText); + + await expect(postPage.commentsSection.mainForm).toBeVisible(); + await expect(postPage.commentsSection.ctaBox).toBeHidden(); + + await expect(postPage.commentsSection.comments.first()).toContainText(commentText); + }); + }); + + test.describe('comments disabled', () => { + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('off'); + }); + + test('comments section is not visible', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForPostToLoad(); + + await expect(postPage.commentsSection.commentsIframe).toBeHidden(); + }); + }); +}); diff --git a/e2e/tests/public/comments.test.ts b/e2e/tests/public/comments.test.ts new file mode 100644 index 0000000..9a83672 --- /dev/null +++ b/e2e/tests/public/comments.test.ts @@ -0,0 +1,80 @@ +import { + CommentFactory, + MemberFactory, + PostFactory, + TierFactory, + createCommentFactory, + createMemberFactory, + createPostFactory, + createTierFactory +} from '@/data-factory'; +import {PostPage} from '@/public-pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, test} from '@/helpers/playwright'; + +test.describe('Ghost Public - Comments - Sorting', () => { + let commentFactory: CommentFactory; + let postFactory: PostFactory; + let memberFactory: MemberFactory; + let tierFactory: TierFactory; + let settingsService: SettingsService; + + test.beforeEach(async ({page}) => { + postFactory = createPostFactory(page.request); + memberFactory = createMemberFactory(page.request); + commentFactory = createCommentFactory(page.request); + tierFactory = createTierFactory(page.request); + settingsService = new SettingsService(page.request); + }); + + test.beforeEach(async () => { + await settingsService.setCommentsEnabled('all'); + }); + + test('sort comments by date and show more', async ({page}) => { + const post = await postFactory.create({status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({status: 'comped', tiers: [{id: paidTier.id}]}); + + const comments = Array.from({length: 25}, (_, index) => { + return { + html: `Test comment ${index + 1}`, + post_id: post.id, + member_id: Math.random() > 0.5 ? member.id : paidMember.id, + created_at: new Date(Date.now() - index * 1000).toISOString() + }; + }); + + await commentFactory.createMany(comments); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + await postPage.waitForCommentsToLoad(); + const postCommentsSection = postPage.commentsSection; + + // verify sorting by oldest comments and load more comments + await postCommentsSection.sortBy('Oldest'); + await expect(postCommentsSection.sortingButton).toContainText('Oldest'); + await expect(postCommentsSection.comments.first()).toContainText('Test comment 25'); + await expect(postCommentsSection.comments.last()).toContainText('Test comment 6'); + await expect(postCommentsSection.showMoreCommentsButton).toBeVisible(); + await expect(postCommentsSection.showMoreCommentsButton).toContainText('Load more (5)'); + + await postCommentsSection.showMoreCommentsButton.click(); + await expect(postCommentsSection.comments).toHaveCount(25); + await expect(postCommentsSection.comments.last()).toContainText('Test comment 1'); + + // verify sorting by newest comments and load more comments + await postCommentsSection.sortBy('Newest'); + await expect(postCommentsSection.sortingButton).toContainText('Newest'); + await expect(postCommentsSection.comments.first()).toContainText('Test comment 1'); + await expect(postCommentsSection.comments.last()).toContainText('Test comment 20'); + await expect(postCommentsSection.showMoreCommentsButton).toBeVisible(); + await expect(postCommentsSection.showMoreCommentsButton).toContainText('Load more (5)'); + + await postCommentsSection.showMoreCommentsButton.click(); + await expect(postCommentsSection.comments).toHaveCount(25); + await expect(postCommentsSection.comments.last()).toContainText('Test comment 25'); + }); +}); diff --git a/e2e/tests/public/homepage.test.ts b/e2e/tests/public/homepage.test.ts new file mode 100644 index 0000000..bf6fe22 --- /dev/null +++ b/e2e/tests/public/homepage.test.ts @@ -0,0 +1,12 @@ +import {HomePage} from '@/public-pages'; +import {expect, test} from '@/helpers/playwright'; + +test.describe('Ghost Public - Homepage', () => { + test('loads correctly', async ({page}) => { + const homePage = new HomePage(page); + + await homePage.goto(); + await expect(homePage.title).toBeVisible(); + await expect(homePage.mainSubscribeButton).toBeVisible(); + }); +}); diff --git a/e2e/tests/public/member-signup-types.test.ts b/e2e/tests/public/member-signup-types.test.ts new file mode 100644 index 0000000..bd952fd --- /dev/null +++ b/e2e/tests/public/member-signup-types.test.ts @@ -0,0 +1,132 @@ +import {EmailClient, MailPit} from '@/helpers/services/email/mail-pit'; +import {HomePage, PublicPage} from '@/public-pages'; +import {MemberDetailsPage, MembersPage} from '@/admin-pages'; +import {Page} from '@playwright/test'; +import {PostFactory, createPostFactory} from '@/data-factory'; +import {expect, signupViaPortal, test} from '@/helpers/playwright'; +import {extractMagicLink} from '@/helpers/services/email/utils'; + +test.describe('Ghost Public - Member Signup - Types', () => { + let emailClient: EmailClient; + + test.beforeEach(async () => { + emailClient = new MailPit(); + }); + + async function finishSignupByMagicLinkInEmail(page: Page, emailAddress: string) { + const messages = await emailClient.searchByRecipient(emailAddress); + const latestMessage = await emailClient.getMessageDetailed(messages[0]); + const emailTextBody = latestMessage.Text; + + const magicLink = extractMagicLink(emailTextBody); + const publicPage = new PublicPage(page); + await publicPage.goto(magicLink); + await new HomePage(page).waitUntilLoaded(); + } + + test('signed up with magic link - direct', async ({page}) => { + await new HomePage(page).goto(); + const {emailAddress, name} = await signupViaPortal(page); + + await finishSignupByMagicLinkInEmail(page, emailAddress); + + const homePage = new HomePage(page); + await expect(homePage.accountButton).toBeVisible(); + + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(emailAddress); + + const membersDetailsPage = new MemberDetailsPage(page); + + await expect(membersDetailsPage.body).toContainText(/Source.*—.*Direct/); + await expect(membersDetailsPage.body).toContainText(/Page.*—.*homepage/); + await expect(membersDetailsPage.nameInput).toHaveValue(name); + }); + + test('signed up with magic link - direct from post', async ({page}) => { + const postFactory: PostFactory = createPostFactory(page.request); + const post = await postFactory.create({title: 'Test Post', status: 'published'}); + + const homePage = new HomePage(page); + await homePage.goto(); + await homePage.linkWithPostName(post.title).click(); + const {emailAddress, name} = await signupViaPortal(page); + + await finishSignupByMagicLinkInEmail(page, emailAddress); + + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(emailAddress); + + const membersDetailsPage = new MemberDetailsPage(page); + + await expect(membersDetailsPage.body).toContainText(/Source.*—.*Direct/); + await expect(membersDetailsPage.body).toContainText(/Page.*—.*Test Post/); + await expect(membersDetailsPage.nameInput).toHaveValue(name); + }); + + test('signed up with magic link - from referrer', async ({page}) => { + const postFactory: PostFactory = createPostFactory(page.request); + const post = await postFactory.create({title: 'Google Test Post', status: 'published'}); + + const homePage = new HomePage(page); + await homePage.goto('/', {referer: 'https://www.google.com', waitUntil: 'domcontentloaded'}); + await homePage.linkWithPostName(post.title).click(); + const {emailAddress, name} = await signupViaPortal(page); + + await finishSignupByMagicLinkInEmail(page, emailAddress); + + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(emailAddress); + + const membersDetailsPage = new MemberDetailsPage(page); + + await expect(membersDetailsPage.body).toContainText(/Source.*—.*Google/); + await expect(membersDetailsPage.body).toContainText(/Page.*—.*Google Test Post/); + await expect(membersDetailsPage.nameInput).toHaveValue(name); + }); + + test('signed up with magic link - direct from newsletter', async ({page}) => { + const postFactory: PostFactory = createPostFactory(page.request); + const post = await postFactory.create({title: 'Newsletter Post', status: 'published'}); + + const homePage = new HomePage(page); + await homePage.goto(`${post.slug}?ref=ghost-newsletter`); + const {emailAddress, name} = await signupViaPortal(page); + + await finishSignupByMagicLinkInEmail(page, emailAddress); + + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(emailAddress); + + const membersDetailsPage = new MemberDetailsPage(page); + + await expect(membersDetailsPage.body).toContainText(/Source.*—.*ghost newsletter/); + await expect(membersDetailsPage.body).toContainText(/Page.*—.*Newsletter Post/); + await expect(membersDetailsPage.nameInput).toHaveValue(name); + }); + + test('signed up with magic link - utm_source=twitter', async ({page}) => { + const postFactory: PostFactory = createPostFactory(page.request); + const post = await postFactory.create({title: 'UTM Source Post', status: 'published'}); + + const homePage = new HomePage(page); + await homePage.goto(`${post.slug}?utm_source=twitter`); + const {emailAddress, name} = await signupViaPortal(page); + + await finishSignupByMagicLinkInEmail(page, emailAddress); + + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(emailAddress); + + const membersDetailsPage = new MemberDetailsPage(page); + + await expect(membersDetailsPage.body).toContainText(/Source.*—.*Twitter/); + await expect(membersDetailsPage.body).toContainText(/Page.*—.*UTM Source Post/); + await expect(membersDetailsPage.nameInput).toHaveValue(name); + }); +}); diff --git a/e2e/tests/public/member-signup.test.ts b/e2e/tests/public/member-signup.test.ts new file mode 100644 index 0000000..db8932b --- /dev/null +++ b/e2e/tests/public/member-signup.test.ts @@ -0,0 +1,44 @@ +import {EmailClient, MailPit} from '@/helpers/services/email/mail-pit'; +import {HomePage, PublicPage} from '@/public-pages'; +import {expect, test} from '@/helpers/playwright'; +import {extractMagicLink} from '@/helpers/services/email/utils'; +import {signupViaPortal} from '@/helpers/playwright/flows/signup'; + +test.describe('Ghost Public - Member Signup', () => { + let emailClient: EmailClient; + + test.beforeEach(async () => { + emailClient = new MailPit(); + }); + + async function retrieveLatestEmailMessage(emailAddress: string, timeoutMs: number = 10000) { + const messages = await emailClient.searchByRecipient(emailAddress, {timeoutMs: timeoutMs}); + return await emailClient.getMessageDetailed(messages[0]); + } + + test('signed up with magic link in email', async ({page}) => { + const homePage = new HomePage(page); + await homePage.goto(); + const {emailAddress} = await signupViaPortal(page); + + const latestMessage = await retrieveLatestEmailMessage(emailAddress); + const emailTextBody = latestMessage.Text; + + const magicLink = extractMagicLink(emailTextBody); + const publicPage = new PublicPage(page); + await publicPage.goto(magicLink); + await homePage.waitUntilLoaded(); + + await expect(homePage.accountButton).toBeVisible(); + }); + + test('received complete the signup email', async ({page}) => { + await new HomePage(page).goto(); + const {emailAddress} = await signupViaPortal(page); + const latestMessage = await retrieveLatestEmailMessage(emailAddress); + expect(latestMessage.Subject.toLowerCase()).toContain('complete'); + + const emailTextBody = latestMessage.Text; + expect(emailTextBody).toContain('complete the signup process'); + }); +}); diff --git a/e2e/tests/public/portal-donations.test.ts b/e2e/tests/public/portal-donations.test.ts new file mode 100644 index 0000000..dbd1f83 --- /dev/null +++ b/e2e/tests/public/portal-donations.test.ts @@ -0,0 +1,92 @@ +import { + FakeStripeCheckoutPage, + HomePage, + SignUpPage, + SupportNotificationPage, + SupportSuccessPage +} from '@/helpers/pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import { + completeDonationViaFakeCheckout, + expect, + signInAsMember, + test +} from '@/helpers/playwright'; +import {createMemberFactory} from '@/data-factory'; + +test.describe('Ghost Public - Portal Donations', () => { + test.use({stripeEnabled: true}); + + test('anonymous donation completes in portal - shows donation success page', async ({page, stripe}) => { + const homePage = new HomePage(page); + await homePage.gotoPortalSupport(); + + const checkoutPage = new FakeStripeCheckoutPage(page); + await checkoutPage.waitUntilDonationReady(); + await expect(checkoutPage.totalAmount).toHaveText('$5.00'); + + await completeDonationViaFakeCheckout(page, stripe!, { + amount: '12.50', + email: `member-donation-${Date.now()}@ghost.org`, + name: 'Test Member Donations' + }); + + const supportSuccessPage = new SupportSuccessPage(page); + await supportSuccessPage.waitForPortalToOpen(); + await expect(supportSuccessPage.title).toBeVisible(); + + await supportSuccessPage.signUpButton.click(); + + const signUpPage = new SignUpPage(page); + await expect(signUpPage.emailInput).toBeVisible(); + }); + + test('free member donation completes in portal - shows donation notification', async ({page, stripe}) => { + const memberFactory = createMemberFactory(page.request); + const member = await memberFactory.create({ + email: `test.member.donations.${Date.now()}@example.com`, + name: 'Test Member Donations', + note: 'Test Member', + status: 'free' + }); + + await signInAsMember(page, member); + + const homePage = new HomePage(page); + await homePage.gotoPortalSupport(); + + const checkoutPage = new FakeStripeCheckoutPage(page); + await checkoutPage.waitUntilDonationReady(); + await expect(checkoutPage.emailInput).toHaveValue(member.email); + + await completeDonationViaFakeCheckout(page, stripe!, { + amount: '12.50', + name: member.name ?? 'Test Member Donations' + }); + + const notificationPage = new SupportNotificationPage(page); + await expect(notificationPage.successMessage).toBeVisible(); + }); + + test('fixed donation amount and currency open donation checkout - shows fixed euro amount', async ({page, stripe}) => { + const settingsService = new SettingsService(page.request); + await settingsService.setDonationsSuggestedAmount(9800); + await settingsService.setDonationsCurrency('EUR'); + + const homePage = new HomePage(page); + await homePage.gotoPortalSupport(); + + const checkoutPage = new FakeStripeCheckoutPage(page); + await checkoutPage.waitUntilDonationReady(); + await expect(checkoutPage.totalAmount).toHaveText('€98.00'); + + await completeDonationViaFakeCheckout(page, stripe!, { + email: `member-donation-fixed-${Date.now()}@ghost.org`, + name: 'Fixed Amount Donor' + }); + + const supportSuccessPage = new SupportSuccessPage(page); + await supportSuccessPage.waitForPortalToOpen(); + await expect(supportSuccessPage.title).toBeVisible(); + }); +}); diff --git a/e2e/tests/public/portal-loading.test.ts b/e2e/tests/public/portal-loading.test.ts new file mode 100644 index 0000000..760d888 --- /dev/null +++ b/e2e/tests/public/portal-loading.test.ts @@ -0,0 +1,70 @@ +import {HomePage, SignInPage, SignUpPage} from '@/helpers/pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {createPaidPortalTier, expect, test} from '@/helpers/playwright'; + +test.describe('Portal Loading', () => { + test.describe('opened Portal', function () { + test('via Subscribe button', async ({page}) => { + const homePage = new HomePage(page); + await homePage.goto(); + + await homePage.openPortalViaSubscribeButton(); + + const signUpPage = new SignUpPage(page); + await expect(signUpPage.emailInput).toBeVisible(); + await expect(signUpPage.signupButton).toBeVisible(); + }); + + test('via Sign in link', async ({page}) => { + const homePage = new HomePage(page); + await homePage.goto(); + + await homePage.openPortalViaSignInLink(); + + const signInPage = new SignInPage(page); + await expect(signInPage.emailInput).toBeVisible(); + await expect(signInPage.continueButton).toBeVisible(); + }); + }); + + test('switch between signup and sign in modes', async ({page}) => { + const homePage = new HomePage(page); + await homePage.goto(); + + await homePage.openPortalViaSubscribeButton(); + + const signUpPage = new SignUpPage(page); + await expect(signUpPage.emailInput).toBeVisible(); + await expect(signUpPage.signupButton).toBeVisible(); + + await signUpPage.signinLink.click(); + + const signInPage = new SignInPage(page); + await expect(signInPage.emailInput).toBeVisible(); + await expect(signInPage.continueButton).toBeVisible(); + }); + + test.describe('signup access', () => { + test.use({stripeEnabled: true}); + + test('invite-only access with paid trial tier - hides free trial message', async ({page}) => { + const settingsService = new SettingsService(page.request); + await createPaidPortalTier(page.request, { + name: `Invite Only Trial Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 100, + yearly_price: 1000, + trial_days: 5 + }); + await settingsService.setMembersSignupAccess('invite'); + + const homePage = new HomePage(page); + await homePage.gotoPortalSignup(); + + const signUpPage = new SignUpPage(page); + await signUpPage.waitForPortalToOpen(); + await expect(signUpPage.inviteOnlyNotification).toHaveText(/contact the owner for access/i); + await expect(signUpPage.freeTrialNotification).toBeHidden(); + }); + }); +}); diff --git a/e2e/tests/public/portal-offers.test.ts b/e2e/tests/public/portal-offers.test.ts new file mode 100644 index 0000000..d036b82 --- /dev/null +++ b/e2e/tests/public/portal-offers.test.ts @@ -0,0 +1,223 @@ +import {type AdminOffer, createOfferFactory} from '@/data-factory'; +import {PortalOfferPage, PublicPage} from '@/helpers/pages'; +import {createPaidPortalTier, createPortalSignupOffer, expect, redeemOfferViaPortal, test} from '@/helpers/playwright'; + +const MEMBER_NAME = 'Testy McTesterson'; + +type OfferLandingExpectation = { + title: string; + discountLabel: string | RegExp; + message: string | RegExp; + updatedPrice?: string | RegExp; +}; + +type RedeemedOfferResult = Awaited>; + +type DiscountOfferExpectation = { + duration: 'once' | 'repeating' | 'forever'; + durationInMonths?: number | null; + priceLabel: string; + timingLabel: string; +}; + +async function expectOfferLandingPage(offerPage: PortalOfferPage, expected: OfferLandingExpectation): Promise { + await offerPage.waitForOfferPage(); + await expect(offerPage.offerTitle).toHaveText(expected.title); + await expect(offerPage.discountLabel).toContainText(expected.discountLabel); + await expect(offerPage.offerMessage).toContainText(expected.message); + + if (expected.updatedPrice) { + await expect(offerPage.updatedPrice).toContainText(expected.updatedPrice); + } +} + +function expectOfferMetadata(result: RedeemedOfferResult, offer: AdminOffer): void { + expect(result.subscription.offer?.id).toBe(offer.id); + expect(result.subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); +} + +async function expectTrialOfferRedemption(result: RedeemedOfferResult, offer: AdminOffer): Promise { + await expect(result.accountPage.freeTrialLabel).toBeVisible(); + expectOfferMetadata(result, offer); + expect(result.subscription.status).toBe('trialing'); +} + +async function expectDiscountOfferRedemption( + result: RedeemedOfferResult, + offer: AdminOffer, + expected: DiscountOfferExpectation +): Promise { + await expect(result.accountPage.offerLabel).toContainText(expected.priceLabel); + await expect(result.accountPage.offerLabel).toContainText(expected.timingLabel); + + expectOfferMetadata(result, offer); + expect(result.subscription.offer?.duration).toBe(expected.duration); + + if (expected.durationInMonths !== undefined) { + expect(result.subscription.offer?.duration_in_months).toBe(expected.durationInMonths); + } +} + +test.describe('Ghost Public - Portal Offers', () => { + test.use({stripeEnabled: true}); + + test('archived offer link opens site - does not open portal offer flow', async ({page, stripe}) => { + const offerFactory = createOfferFactory(page.request); + const publicPage = new PublicPage(page); + const tier = await createPaidPortalTier(page.request, { + name: `Archived Offer Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 600, + yearly_price: 6000 + }, { + stripe: stripe! + }); + const offer = await offerFactory.create({ + name: 'Archived Offer', + code: `archived-offer-${Date.now()}`, + cadence: 'month', + amount: 10, + duration: 'once', + type: 'percent', + tierId: tier.id + }); + + await offerFactory.update(offer.id, {status: 'archived'}); + + await publicPage.gotoOfferCode(offer.code); + await publicPage.portalRoot.waitFor({state: 'attached'}); + + await expect(publicPage.portalPopupFrame).toHaveCount(0); + await expect(page).not.toHaveURL(/#\/portal\/offers\//); + }); + + test('retention offer link opens site - does not open portal offer flow', async ({page}) => { + const offerFactory = createOfferFactory(page.request); + const publicPage = new PublicPage(page); + const offer = await offerFactory.create({ + name: 'Retention Offer', + code: `retention-offer-${Date.now()}`, + cadence: 'month', + amount: 10, + duration: 'once', + type: 'percent', + redemption_type: 'retention', + tierId: null + }); + + await publicPage.gotoOfferCode(offer.code); + await publicPage.portalRoot.waitFor({state: 'attached'}); + + await expect(publicPage.portalPopupFrame).toHaveCount(0); + await expect(page).not.toHaveURL(/#\/portal\/offers\//); + }); + + test('free trial offer opens in portal - redemption shows free trial state', async ({page, stripe}) => { + const offer = await createPortalSignupOffer(page.request, stripe!, { + amount: 14, + codePrefix: 'trial-offer', + duration: 'trial', + tierNamePrefix: 'Trial Offer Tier', + type: 'trial' + }); + + const publicPage = new PublicPage(page); + await publicPage.gotoOfferCode(offer.code); + + const offerPage = new PortalOfferPage(page); + await expectOfferLandingPage(offerPage, { + title: offer.display_title ?? offer.name, + discountLabel: '14 days free', + message: 'Try free for 14 days' + }); + + const redemption = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); + await expectTrialOfferRedemption(redemption, offer); + }); + + test('one-time discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { + const offer = await createPortalSignupOffer(page.request, stripe!, { + amount: 10, + codePrefix: 'once-offer', + duration: 'once', + tierNamePrefix: 'One-time Offer Tier', + type: 'percent' + }); + + const publicPage = new PublicPage(page); + await publicPage.gotoOfferCode(offer.code); + + const offerPage = new PortalOfferPage(page); + await expectOfferLandingPage(offerPage, { + title: offer.display_title ?? offer.name, + discountLabel: '10% off', + message: '10% off for first month', + updatedPrice: /\$5\.40/ + }); + + const redemption = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); + await expectDiscountOfferRedemption(redemption, offer, { + duration: 'once', + priceLabel: '$5.40/month', + timingLabel: 'Ends' + }); + }); + + test('repeating discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { + const offer = await createPortalSignupOffer(page.request, stripe!, { + amount: 10, + codePrefix: 'repeating-offer', + duration: 'repeating', + duration_in_months: 3, + tierNamePrefix: 'Repeating Offer Tier', + type: 'percent' + }); + + const publicPage = new PublicPage(page); + await publicPage.gotoOfferCode(offer.code); + + const offerPage = new PortalOfferPage(page); + await expectOfferLandingPage(offerPage, { + title: offer.display_title ?? offer.name, + discountLabel: '10% off', + message: '10% off for first 3 months', + updatedPrice: /\$5\.40/ + }); + + const redemption = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); + await expectDiscountOfferRedemption(redemption, offer, { + duration: 'repeating', + durationInMonths: 3, + priceLabel: '$5.40/month', + timingLabel: 'Ends' + }); + }); + + test('forever discount offer opens in portal - redemption shows discounted plan label', async ({page, stripe}) => { + const offer = await createPortalSignupOffer(page.request, stripe!, { + amount: 10, + codePrefix: 'forever-offer', + duration: 'forever', + tierNamePrefix: 'Forever Offer Tier', + type: 'percent' + }); + + const publicPage = new PublicPage(page); + await publicPage.gotoOfferCode(offer.code); + + const offerPage = new PortalOfferPage(page); + await expectOfferLandingPage(offerPage, { + title: offer.display_title ?? offer.name, + discountLabel: '10% off', + message: '10% off forever', + updatedPrice: /\$5\.40/ + }); + + const redemption = await redeemOfferViaPortal(page, stripe!, {name: MEMBER_NAME}); + await expectDiscountOfferRedemption(redemption, offer, { + duration: 'forever', + priceLabel: '$5.40/month', + timingLabel: 'Forever' + }); + }); +}); diff --git a/e2e/tests/public/portal-script-loading.test.ts b/e2e/tests/public/portal-script-loading.test.ts new file mode 100644 index 0000000..b91b943 --- /dev/null +++ b/e2e/tests/public/portal-script-loading.test.ts @@ -0,0 +1,42 @@ +import {HomePage} from '@/helpers/pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, test} from '@/helpers/playwright'; + +test.describe('Portal Script Loading', () => { + test('memberships enabled - loads portal script', async ({page}) => { + const settingsService = new SettingsService(page.request); + await settingsService.setMembersSignupAccess('all'); + + const homePage = new HomePage(page); + await homePage.gotoPortalSignup(); + + await expect(homePage.portalScript).toHaveAttribute('src', /\/portal\.min\.js$/); + await expect(homePage.portalIframe).toHaveCount(1); + }); + + test.describe('with stripe connected', () => { + test.use({stripeEnabled: true}); + + test('memberships disabled - loads portal script', async ({page}) => { + const settingsService = new SettingsService(page.request); + await settingsService.setMembersSignupAccess('none'); + + const homePage = new HomePage(page); + await homePage.gotoPortalSignup(); + + await expect(homePage.portalScript).toHaveAttribute('src', /\/portal\.min\.js$/); + await expect(homePage.portalIframe).toHaveCount(1); + }); + }); + + test('memberships and donations disabled - does not load portal script', async ({page}) => { + const settingsService = new SettingsService(page.request); + await settingsService.setMembersSignupAccess('none'); + + const homePage = new HomePage(page); + await homePage.gotoPortalSignup(); + + await expect(homePage.portalScript).toHaveCount(0); + await expect(homePage.portalIframe).toHaveCount(0); + }); +}); diff --git a/e2e/tests/public/portal-tiers.test.ts b/e2e/tests/public/portal-tiers.test.ts new file mode 100644 index 0000000..9424856 --- /dev/null +++ b/e2e/tests/public/portal-tiers.test.ts @@ -0,0 +1,29 @@ +import {HomePage, PortalAccountPage} from '@/helpers/pages'; +import {completePaidSignupViaPortal, createPaidPortalTier, expect, test} from '@/helpers/playwright'; + +test.describe('Ghost Public - Portal Tiers', () => { + test.use({stripeEnabled: true}); + + test('single paid tier signup via portal completes checkout - portal shows billing info', async ({page, stripe}) => { + await createPaidPortalTier(page.request, { + name: `Portal Tier ${Date.now()}`, + visibility: 'public', + currency: 'usd', + monthly_price: 500, + yearly_price: 5000 + }); + + const name = 'Testy McTesterson'; + const {emailAddress} = await completePaidSignupViaPortal(page, stripe!, {name}); + + const homePage = new HomePage(page); + await homePage.goto(); + await homePage.openAccountPortal(); + + const portalAccountPage = new PortalAccountPage(page); + await portalAccountPage.waitForPortalToOpen(); + await expect(portalAccountPage.emailText(emailAddress)).toBeVisible(); + await expect(portalAccountPage.billingInfoHeading).toBeVisible(); + await expect(portalAccountPage.cardLast4('4242')).toBeVisible(); + }); +}); diff --git a/e2e/tests/public/portal-upgrade.test.ts b/e2e/tests/public/portal-upgrade.test.ts new file mode 100644 index 0000000..d3937e9 --- /dev/null +++ b/e2e/tests/public/portal-upgrade.test.ts @@ -0,0 +1,108 @@ +import {HomePage, PortalAccountPage} from '@/helpers/pages'; +import { + completePaidSignupViaPortal, + completePaidUpgradeViaPortal, + createPaidPortalTier, + expect, + switchPlanViaPortal, + test +} from '@/helpers/playwright'; +import {createMemberFactory} from '@/data-factory'; + +test.describe('Ghost Public - Portal Upgrade', () => { + test.use({stripeEnabled: true}); + + test('free member upgrades to paid via portal - portal shows billing info', async ({page, stripe}) => { + const memberFactory = createMemberFactory(page.request); + const tier = await createPaidPortalTier(page.request, { + name: `Free Upgrade Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 500, + yearly_price: 5000 + }); + const member = await memberFactory.create({ + email: `free-upgrade-${Date.now()}@example.com`, + name: 'Free Upgrade Member', + status: 'free' + }); + + await completePaidUpgradeViaPortal(page, stripe!, member, { + cadence: 'yearly', + tierName: tier.name + }); + + const homePage = new HomePage(page); + await homePage.goto(); + await homePage.openAccountPortal(); + + const portalAccountPage = new PortalAccountPage(page); + await portalAccountPage.waitForPortalToOpen(); + await expect(portalAccountPage.emailText(member.email)).toBeVisible(); + await expect(portalAccountPage.planPrice('$50.00/year')).toBeVisible(); + await expect(portalAccountPage.billingInfoHeading).toBeVisible(); + await expect(portalAccountPage.cardLast4('4242')).toBeVisible(); + }); + + test('comped member upgrades to paid via portal - portal shows billing info', async ({page, stripe}) => { + const memberFactory = createMemberFactory(page.request); + const tier = await createPaidPortalTier(page.request, { + name: `Comped Upgrade Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 500, + yearly_price: 5000 + }); + const member = await memberFactory.create({ + email: `comped-upgrade-${Date.now()}@example.com`, + name: 'Comped Upgrade Member', + status: 'comped', + tiers: [{id: tier.id}] + }); + + await completePaidUpgradeViaPortal(page, stripe!, member, { + cadence: 'yearly', + tierName: tier.name + }); + + const homePage = new HomePage(page); + await homePage.goto(); + await homePage.openAccountPortal(); + + const portalAccountPage = new PortalAccountPage(page); + await portalAccountPage.waitForPortalToOpen(); + await expect(portalAccountPage.emailText(member.email)).toBeVisible(); + await expect(portalAccountPage.planPrice('$50.00/year')).toBeVisible(); + await expect(portalAccountPage.billingInfoHeading).toBeVisible(); + await expect(portalAccountPage.cardLast4('4242')).toBeVisible(); + }); + + test('paid member changes plan in portal - subscription switches between monthly and yearly', async ({page, stripe}) => { + const tier = await createPaidPortalTier(page.request, { + name: `Upgrade Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 500, + yearly_price: 5000 + }); + const name = 'Portal Plan Switch Member'; + const {emailAddress} = await completePaidSignupViaPortal(page, stripe!, { + cadence: 'yearly', + name, + tierName: tier.name + }); + + await switchPlanViaPortal(page, { + cadence: 'monthly', + tierName: tier.name + }); + + const portalAccountPage = new PortalAccountPage(page); + await expect(portalAccountPage.emailText(emailAddress)).toBeVisible(); + await expect(portalAccountPage.planPrice('$5.00/month')).toBeVisible(); + + await switchPlanViaPortal(page, { + cadence: 'yearly', + tierName: tier.name + }); + + await expect(portalAccountPage.planPrice('$50.00/year')).toBeVisible(); + }); +}); diff --git a/e2e/tests/public/stripe-donation-checkout.test.ts b/e2e/tests/public/stripe-donation-checkout.test.ts new file mode 100644 index 0000000..2e3acbf --- /dev/null +++ b/e2e/tests/public/stripe-donation-checkout.test.ts @@ -0,0 +1,93 @@ +import {EmailClient, MailPit} from '@/helpers/services/email/mail-pit'; +import {FakeStripeCheckoutPage} from '@/helpers/pages'; +import {expect, test} from '@/helpers/playwright'; + +interface CheckoutSessionResponse { + url: string; +} + +// This is a harness smoke test for the e2e Stripe tooling rather than a long-term +// product-facing spec. Migrated donation tests should carry the readable behavior +// coverage, and this should stay thin or be removed if it becomes redundant. +// TODO: REMOVE TEST + +test.describe('Ghost Public - Stripe Donation Checkout', () => { + test.use({stripeEnabled: true}); + + let emailClient: EmailClient; + + test.beforeEach(async () => { + emailClient = new MailPit(); + }); + + test('donation checkout uses fake stripe payment mode - completed webhook sends staff email', async ({page, stripe}) => { + const donorName = `Donation Donor ${Date.now()}`; + const donorEmail = `donation-${Date.now()}@example.com`; + const donationMessage = `Keep building ${Date.now()}`; + const personalNote = 'Leave a note for the publisher'; + + const response = await page.request.post('/members/api/create-stripe-checkout-session/', { + data: { + type: 'donation', + customerEmail: donorEmail, + successUrl: 'http://localhost/success', + cancelUrl: 'http://localhost/cancel', + personalNote + } + }); + + expect(response.ok()).toBe(true); + + const sessionResponse = await response.json() as CheckoutSessionResponse; + const products = stripe!.getProducts(); + const prices = stripe!.getPrices(); + const sessions = stripe!.getCheckoutSessions(); + + expect(products).toHaveLength(1); + expect(prices).toHaveLength(1); + expect(sessions).toHaveLength(1); + + const price = prices[0]; + const session = sessions[0]; + + expect(price.type).toBe('one_time'); + expect(price.currency).toBe('usd'); + expect(price.unit_amount).toBeNull(); + expect(price.custom_unit_amount?.enabled).toBe(true); + expect(price.custom_unit_amount?.preset).toBe(500); + + expect(session.request.line_items?.[0]?.price).toBe(price.id); + expect(session.request.submit_type).toBe('pay'); + expect(session.request.invoice_creation?.enabled).toBe(true); + expect(session.request.invoice_creation?.invoice_data?.metadata.ghost_donation).toBe('true'); + expect(session.request.custom_fields?.[0]?.key).toBe('donation_message'); + expect(session.request.custom_fields?.[0]?.label?.custom).toBe(personalNote); + expect(session.response.mode).toBe('payment'); + expect(session.response.customer).toBeNull(); + expect(session.response.customer_email).toBe(donorEmail); + expect(session.response.metadata.ghost_donation).toBe('true'); + expect(sessionResponse.url).toBe(session.response.url); + + const fakeCheckoutPage = new FakeStripeCheckoutPage(page); + await fakeCheckoutPage.goto(sessionResponse.url); + await fakeCheckoutPage.waitUntilDonationReady(); + + await stripe!.completeLatestDonationCheckout({ + donationMessage, + email: donorEmail, + name: donorName + }); + + const messages = await emailClient.search({ + subject: donorName + }, { + timeoutMs: 10000 + }); + expect(messages.length).toBeGreaterThan(0); + const latestMessage = await emailClient.getMessageDetailed(messages[0]); + + expect(latestMessage.Subject).toContain('One-time payment received'); + expect(latestMessage.Subject).toContain(donorName); + expect(latestMessage.Text).toContain(donationMessage); + }); +}); diff --git a/e2e/tests/public/stripe-offer-checkout.test.ts b/e2e/tests/public/stripe-offer-checkout.test.ts new file mode 100644 index 0000000..7e96cbf --- /dev/null +++ b/e2e/tests/public/stripe-offer-checkout.test.ts @@ -0,0 +1,83 @@ +import {MembersService} from '@/helpers/services/members/members-service'; +import {createOfferFactory, createTierFactory} from '@/data-factory'; +import {expect, test} from '@/helpers/playwright'; + +interface CheckoutSessionResponse { + url: string; +} + +// This is a harness smoke test for the e2e Stripe tooling rather than a long-term +// product-facing spec. Migrated offer tests should carry the readable behavior +// coverage, and this should stay thin or be removed if it becomes redundant. +// TODO: REMOVE TEST + +test.describe('Ghost Public - Stripe Offer Checkout', () => { + test.use({stripeEnabled: true}); + + test('offer checkout creates a fake stripe coupon - redeemed offer is linked to the subscription', async ({page, stripe}) => { + const offerFactory = createOfferFactory(page.request); + const tierFactory = createTierFactory(page.request); + const membersService = new MembersService(page.request); + const tierName = `Offer Tier ${Date.now()}`; + const memberEmail = `offer-checkout-${Date.now()}@example.com`; + + const tier = await tierFactory.create({ + name: tierName, + currency: 'usd', + monthly_price: 600, + yearly_price: 6000 + }); + + const offer = await offerFactory.create({ + name: 'Spring Offer', + code: `spring-offer-${Date.now()}`, + cadence: 'month', + amount: 10, + duration: 'repeating', + duration_in_months: 3, + type: 'percent', + tierId: tier.id + }); + + const response = await page.request.post('/members/api/create-stripe-checkout-session/', { + data: { + customerEmail: memberEmail, + offerId: offer.id, + successUrl: 'http://localhost/success', + cancelUrl: 'http://localhost/cancel' + } + }); + + expect(response.ok()).toBe(true); + + const sessionResponse = await response.json() as CheckoutSessionResponse; + const session = stripe!.getCheckoutSessions().at(-1); + + expect(session).toBeDefined(); + expect(sessionResponse.url).toBe(session?.response.url); + expect(session?.response.metadata.offer).toBe(offer.id); + + const couponId = session?.request.discounts?.[0]?.coupon; + expect(couponId).toBeDefined(); + + const coupon = stripe!.getCoupons().find(item => item.id === couponId); + expect(coupon).toBeDefined(); + expect(coupon?.duration).toBe('repeating'); + expect(coupon?.duration_in_months).toBe(3); + expect(coupon?.percent_off).toBe(10); + expect(session?.request.subscription_data?.items).toHaveLength(1); + + const createdMember = await stripe!.completeLatestSubscriptionCheckout({name: 'Offer Member'}); + expect(createdMember.subscription.discount?.coupon.id).toBe(coupon?.id); + expect(createdMember.subscription.discount?.end).not.toBeNull(); + + const member = await membersService.getByEmailWithSubscriptions(memberEmail); + const subscription = member.subscriptions[0]; + + expect(subscription.offer?.id).toBe(offer.id); + expect(subscription.offer_redemptions?.some(item => item.id === offer.id)).toBe(true); + expect(subscription.next_payment?.original_amount).toBe(600); + expect(subscription.next_payment?.amount).toBe(540); + expect(subscription.next_payment?.discount?.offer_id).toBe(offer.id); + }); +}); diff --git a/e2e/tests/public/stripe-subscription-mutations.test.ts b/e2e/tests/public/stripe-subscription-mutations.test.ts new file mode 100644 index 0000000..bc967b2 --- /dev/null +++ b/e2e/tests/public/stripe-subscription-mutations.test.ts @@ -0,0 +1,123 @@ +import {FakeStripeCheckoutPage, PublicPage} from '@/helpers/pages'; +import {SignUpPage} from '@/portal-pages'; +import {createPaidPortalTier, expect, test} from '@/helpers/playwright'; +import type {Page} from '@playwright/test'; + +async function getMemberIdentityToken(page: Page): Promise { + const response = await page.context().request.get('/members/api/session'); + + expect(response.ok()).toBe(true); + + const identity = await response.text(); + expect(identity).not.toBe(''); + + return identity; +} + +function getAlternateCadence(interval: string | undefined): 'month' | 'year' { + if (interval === 'month') { + return 'year'; + } + + if (interval === 'year') { + return 'month'; + } + + throw new Error(`Unsupported subscription cadence: ${interval ?? 'missing'}`); +} + +function getLatestCheckoutSuccessUrl(stripeCheckoutCount: {response: {success_url: string}}[]): string { + const successUrl = stripeCheckoutCount.at(-1)?.response.success_url; + + if (!successUrl) { + throw new Error('Latest Stripe checkout session does not include a success URL'); + } + + return successUrl; +} + +function getTargetPrice(targetCadence: 'month' | 'year', prices: { + monthly: {id: string}; + yearly: {id: string}; +}): {id: string} { + if (targetCadence === 'month') { + return prices.monthly; + } + + return prices.yearly; +} + +test.describe('Ghost Public - Stripe Subscription Mutations', () => { + test.use({stripeEnabled: true}); + + test('paid member subscription update via ghost - switches the fake stripe price', async ({page, stripe}) => { + const memberEmail = `stripe-mutation-${Date.now()}@example.com`; + const tier = await createPaidPortalTier(page.request, { + name: `Mutation Tier ${Date.now()}`, + currency: 'usd', + monthly_price: 500, + yearly_price: 5000 + }); + + await expect.poll(() => { + return stripe!.getProducts().find(item => item.name === tier.name); + }, {timeout: 10000}).toBeDefined(); + + await expect.poll(() => { + return stripe!.getPrices().filter(item => item.product === stripe!.getProducts().find(product => product.name === tier.name)?.id).length; + }, {timeout: 10000}).toBe(2); + + const product = stripe!.getProducts().find(item => item.name === tier.name); + const monthlyPrice = stripe!.getPrices().find((item) => { + return item.product === product?.id && item.recurring?.interval === 'month'; + }); + const yearlyPrice = stripe!.getPrices().find((item) => { + return item.product === product?.id && item.recurring?.interval === 'year'; + }); + + expect(product).toBeDefined(); + expect(monthlyPrice).toBeDefined(); + expect(yearlyPrice).toBeDefined(); + + const publicPage = new PublicPage(page); + await publicPage.gotoPortalSignup(); + + const signUpPage = new SignUpPage(page); + await signUpPage.waitForPortalToOpen(); + await signUpPage.fillAndSubmitPaidSignup(memberEmail, 'Stripe Mutation Member', tier.name); + + const fakeCheckoutPage = new FakeStripeCheckoutPage(page); + await fakeCheckoutPage.waitUntilLoaded(); + await stripe!.completeLatestSubscriptionCheckout({name: 'Stripe Mutation Member'}); + await page.goto(getLatestCheckoutSuccessUrl(stripe!.getCheckoutSessions())); + + const subscription = stripe!.getSubscriptions().at(-1); + expect(subscription).toBeDefined(); + + const currentPrice = subscription!.items.data[0]?.price; + expect(currentPrice).toBeDefined(); + + const targetCadence = getAlternateCadence(currentPrice!.recurring?.interval); + const targetPrice = getTargetPrice(targetCadence, { + monthly: monthlyPrice!, + yearly: yearlyPrice! + }); + + expect(targetPrice).toBeDefined(); + + const identity = await getMemberIdentityToken(page); + const response = await page.context().request.put(`/members/api/subscriptions/${subscription!.id}/`, { + data: { + identity, + tierId: tier.id, + cadence: targetCadence + } + }); + + expect(response.status()).toBe(204); + + const updatedSubscription = stripe!.getSubscriptions().find(item => item.id === subscription!.id); + expect(updatedSubscription?.items.data[0]?.price.id).toBe(targetPrice!.id); + expect(updatedSubscription?.items.data[0]?.price.recurring?.interval).toBe(targetCadence); + }); +}); diff --git a/e2e/tests/public/stripe-webhook-subscription-lifecycle.test.ts b/e2e/tests/public/stripe-webhook-subscription-lifecycle.test.ts new file mode 100644 index 0000000..3e69125 --- /dev/null +++ b/e2e/tests/public/stripe-webhook-subscription-lifecycle.test.ts @@ -0,0 +1,102 @@ +import {APIRequestContext, Page} from '@playwright/test'; +import {HomePage, MemberDetailsPage, MembersPage, PortalAccountPage} from '@/helpers/pages'; +import {MembersService} from '@/helpers/services/members'; +import {expect, test} from '@/helpers/playwright'; + +async function waitForMemberStatus(request: APIRequestContext, email: string, status: string) { + const membersService = new MembersService(request); + + await expect.poll(async () => { + try { + const member = await membersService.getByEmail(email); + return member.status; + } catch { + return null; + } + }, {timeout: 10000}).toBe(status); +} + +async function waitForCanceledSubscription(request: APIRequestContext, email: string) { + const membersService = new MembersService(request); + + await expect.poll(async () => { + try { + const member = await membersService.getByEmailWithSubscriptions(email); + return member.subscriptions[0]?.cancel_at_period_end ?? null; + } catch { + return null; + } + }, {timeout: 10000}).toBe(true); +} + +async function openPortalAsMember(page: Page, email: string) { + const membersPage = new MembersPage(page); + await membersPage.goto(); + await membersPage.clickMemberByEmail(email); + + const memberDetailsPage = new MemberDetailsPage(page); + await memberDetailsPage.settingsSection.memberActionsButton.click(); + await memberDetailsPage.settingsSection.impersonateButton.click(); + + await expect(memberDetailsPage.magicLinkInput).not.toHaveValue(''); + const magicLink = await memberDetailsPage.magicLinkInput.inputValue(); + await memberDetailsPage.goto(magicLink); + + const homePage = new HomePage(page); + await homePage.openAccountPortal(); + + const portalAccountPage = new PortalAccountPage(page); + await portalAccountPage.waitForPortalToOpen(); + return portalAccountPage; +} + +test.describe('Portal - Stripe Subscription Lifecycle via Webhooks', () => { + test.use({stripeEnabled: true}); + + test('webhook-seeded paid member - sees billing details in portal', async ({page, stripe}) => { + const email = `portal-paid-${Date.now()}@example.com`; + + await stripe!.createPaidMemberViaWebhooks({email, name: 'Portal Paid Member'}); + await waitForMemberStatus(page.request, email, 'paid'); + + const portalAccountPage = await openPortalAsMember(page, email); + + await expect(portalAccountPage.title).toBeVisible(); + await expect(portalAccountPage.emailText(email)).toBeVisible(); + await expect(portalAccountPage.planPrice('$5.00/month')).toBeVisible(); + await expect(portalAccountPage.billingInfoHeading).toBeVisible(); + await expect(portalAccountPage.cardLast4('4242')).toBeVisible(); + }); + + test('cancel-at-period-end webhook - shows canceled state in portal', async ({page, stripe}) => { + const email = `portal-cancel-${Date.now()}@example.com`; + const {subscription} = await stripe!.createPaidMemberViaWebhooks({email, name: 'Portal Cancel Member'}); + + await waitForMemberStatus(page.request, email, 'paid'); + await stripe!.cancelSubscription({subscription}); + await waitForCanceledSubscription(page.request, email); + + const portalAccountPage = await openPortalAsMember(page, email); + + await expect(portalAccountPage.cancellationNotice).toBeVisible(); + await expect(portalAccountPage.resumeSubscriptionButton).toBeVisible(); + await expect(portalAccountPage.canceledBadge).toBeVisible(); + }); + + test('subscription-deleted webhook - shows free membership in portal', async ({page, stripe}) => { + const email = `portal-free-${Date.now()}@example.com`; + const {subscription} = await stripe!.createPaidMemberViaWebhooks({email, name: 'Portal Free Member'}); + + await waitForMemberStatus(page.request, email, 'paid'); + await stripe!.deleteSubscription({subscription}); + await waitForMemberStatus(page.request, email, 'free'); + + const portalAccountPage = await openPortalAsMember(page, email); + + await expect(portalAccountPage.title).toBeVisible(); + await expect(portalAccountPage.emailText(email)).toBeVisible(); + await expect(portalAccountPage.emailNewsletterHeading).toBeVisible(); + await expect(portalAccountPage.billingInfoHeading).toHaveCount(0); + await expect(portalAccountPage.planPrice('$5.00/month')).toHaveCount(0); + }); +}); diff --git a/e2e/tests/public/transistor.test.ts b/e2e/tests/public/transistor.test.ts new file mode 100644 index 0000000..c3ddf68 --- /dev/null +++ b/e2e/tests/public/transistor.test.ts @@ -0,0 +1,90 @@ +import {MemberFactory, PostFactory, TierFactory, createMemberFactory, createPostFactory, createTierFactory} from '@/data-factory'; +import {PostPage} from '@/public-pages'; +import {SettingsService} from '@/helpers/services/settings/settings-service'; +import {expect, test} from '@/helpers/playwright'; +import {signInAsMember} from '@/helpers/playwright/flows/sign-in'; + +test.describe('Ghost Public - Transistor', () => { + test.use({labs: {transistor: true}}); + + let postFactory: PostFactory; + let memberFactory: MemberFactory; + let tierFactory: TierFactory; + let settingsService: SettingsService; + + test.beforeEach(async ({page}) => { + postFactory = createPostFactory(page.request); + memberFactory = createMemberFactory(page.request); + tierFactory = createTierFactory(page.request); + settingsService = new SettingsService(page.request); + + await settingsService.setTransistorEnabled(true); + }); + + test('anonymous visitor - transistor embed is not visible', async ({page}) => { + const post = await postFactory.createWithCards('transistor', {status: 'published'}); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + + await expect(postPage.postContent).toContainText('Before transistor'); + await expect(postPage.postContent).toContainText('After transistor'); + await expect(postPage.transistorCard).toBeHidden(); + await expect(postPage.transistorIframe).toBeHidden(); + }); + + test('free member - transistor embed is visible', async ({page}) => { + const post = await postFactory.createWithCards('transistor', {status: 'published'}); + const member = await memberFactory.create({status: 'free'}); + + await signInAsMember(page, member); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + + await expect(postPage.postContent).toContainText('Before transistor'); + await expect(postPage.postContent).toContainText('After transistor'); + await expect(postPage.transistorIframe).toBeVisible(); + + // The data-src should contain the member's UUID (server-side replacement of %7Buuid%7D) + const dataSrc = await postPage.transistorIframe.getAttribute('data-src'); + expect(dataSrc).not.toContain('%7Buuid%7D'); + expect(dataSrc).toContain(member.uuid); + }); + + test('paid member - transistor embed is visible', async ({page}) => { + const post = await postFactory.createWithCards('transistor', {status: 'published'}); + const paidTier = await tierFactory.getFirstPaidTier(); + const paidMember = await memberFactory.create({ + status: 'comped', + tiers: [{id: paidTier.id}] + }); + + await signInAsMember(page, paidMember); + + const postPage = new PostPage(page); + await postPage.gotoPost(post.slug); + + await expect(postPage.postContent).toContainText('Before transistor'); + await expect(postPage.postContent).toContainText('After transistor'); + await expect(postPage.transistorIframe).toBeVisible(); + + const dataSrc = await postPage.transistorIframe.getAttribute('data-src'); + expect(dataSrc).not.toContain('%7Buuid%7D'); + expect(dataSrc).toContain(paidMember.uuid); + }); + + test('preview mode - shows placeholder instead of iframe', async ({page}) => { + const post = await postFactory.createWithCards('transistor', {status: 'draft'}); + + const postPage = new PostPage(page); + await postPage.goto(`/p/${post.uuid}/?member_status=free`); + await postPage.waitForPostToLoad(); + + await expect(postPage.postContent).toContainText('Before transistor'); + await expect(postPage.postContent).toContainText('After transistor'); + await expect(postPage.transistorPlaceholder).toBeVisible(); + await expect(postPage.transistorPlaceholder).toContainText('Members-only podcasts'); + await expect(postPage.transistorIframe).toBeHidden(); + }); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..b0c9b07 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,118 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext", /* Specify what module code is generated. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@/admin-pages": ["./helpers/pages/admin/index"], + "@/public-pages": ["./helpers/pages/public/index"], + "@/portal-pages": ["./helpers/pages/portal/index"], + "@/helpers/*": ["./helpers/*"], + "@/data-factory": ["./data-factory/index.ts"], + "@/data-factory/*": ["./data-factory/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["helpers/**/*", "tests/**/*", "data-factory/**/*", "types.d.ts"], + "exclude": ["node_modules"] +} diff --git a/e2e/types.d.ts b/e2e/types.d.ts new file mode 100644 index 0000000..e0a3d6e --- /dev/null +++ b/e2e/types.d.ts @@ -0,0 +1,44 @@ +declare module '@tryghost/logging' { + export function error(...args: unknown[]): void; + export function warn(...args: unknown[]): void; + export function info(...args: unknown[]): void; + export function debug(...args: unknown[]): void; +} + +declare module '@tryghost/debug' { + function debug(namespace: string): (...args: unknown[]) => void; + export = debug; +} + +declare module 'busboy' { + import {IncomingHttpHeaders} from 'http'; + import {Writable} from 'stream'; + + interface BusboyConfig { + headers: IncomingHttpHeaders; + highWaterMark?: number; + fileHwm?: number; + defCharset?: string; + preservePath?: boolean; + limits?: { + fieldNameSize?: number; + fieldSize?: number; + fields?: number; + fileSize?: number; + files?: number; + parts?: number; + headerPairs?: number; + }; + } + + interface Busboy extends Writable { + on(event: 'field', listener: (name: string, val: string, info: {nameTruncated: boolean; valueTruncated: boolean; encoding: string; mimeType: string}) => void): this; + on(event: 'file', listener: (name: string, file: NodeJS.ReadableStream, info: {filename: string; encoding: string; mimeType: string}) => void): this; + on(event: 'close', listener: () => void): this; + on(event: 'error', listener: (err: Error) => void): this; + on(event: string, listener: (...args: unknown[]) => void): this; + } + + function busboy(config: BusboyConfig): Busboy; + export = busboy; +} \ No newline at end of file diff --git a/e2e/visual-regression/.auth/.gitkeep b/e2e/visual-regression/.auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/e2e/visual-regression/.gitignore b/e2e/visual-regression/.gitignore new file mode 100644 index 0000000..38ff27a --- /dev/null +++ b/e2e/visual-regression/.gitignore @@ -0,0 +1,6 @@ +# Auth state from Playwright setup +.auth/state.json + +# Playwright test results and reports +test-results/ +playwright-report/ diff --git a/e2e/visual-regression/auth.setup.ts b/e2e/visual-regression/auth.setup.ts new file mode 100644 index 0000000..8e7e9a8 --- /dev/null +++ b/e2e/visual-regression/auth.setup.ts @@ -0,0 +1,29 @@ +import {expect, test as setup} from '@playwright/test'; + +/** + * Authenticates once against the running Ghost instance and saves + * the session to a storage-state file so all visual-regression specs + * can reuse it without logging in again. + * + * Expects the default Ghost development credentials: + * Email: ghost-author@example.com + * Password: Sl1m3rson99 + */ +const AUTH_FILE = './e2e/visual-regression/.auth/state.json'; + +setup('authenticate', async ({page}) => { + const email = process.env.GHOST_ADMIN_EMAIL || 'ghost-author@example.com'; + const password = process.env.GHOST_ADMIN_PASSWORD || 'Sl1m3rson99'; + + await page.goto('/ghost/#/signin'); + await page.waitForLoadState('load'); + + await page.getByRole('textbox', {name: 'Email address'}).fill(email); + await page.getByRole('textbox', {name: 'Password'}).fill(password); + await page.getByRole('button', {name: /Sign in/}).click(); + + // Wait for the admin to fully load (sidebar with navigation links) + await expect(page.getByRole('link', {name: 'Posts'})).toBeVisible({timeout: 30_000}); + + await page.context().storageState({path: AUTH_FILE}); +}); diff --git a/e2e/visual-regression/baselines/activitypub-feed.png b/e2e/visual-regression/baselines/activitypub-feed.png new file mode 100644 index 0000000..3b9c44a Binary files /dev/null and b/e2e/visual-regression/baselines/activitypub-feed.png differ diff --git a/e2e/visual-regression/baselines/activitypub-inbox.png b/e2e/visual-regression/baselines/activitypub-inbox.png new file mode 100644 index 0000000..756c436 Binary files /dev/null and b/e2e/visual-regression/baselines/activitypub-inbox.png differ diff --git a/e2e/visual-regression/baselines/activitypub-notifications.png b/e2e/visual-regression/baselines/activitypub-notifications.png new file mode 100644 index 0000000..ee0b323 Binary files /dev/null and b/e2e/visual-regression/baselines/activitypub-notifications.png differ diff --git a/e2e/visual-regression/baselines/activitypub-profile.png b/e2e/visual-regression/baselines/activitypub-profile.png new file mode 100644 index 0000000..7f41ec0 Binary files /dev/null and b/e2e/visual-regression/baselines/activitypub-profile.png differ diff --git a/e2e/visual-regression/baselines/activitypub.png b/e2e/visual-regression/baselines/activitypub.png new file mode 100644 index 0000000..5be70ed Binary files /dev/null and b/e2e/visual-regression/baselines/activitypub.png differ diff --git a/e2e/visual-regression/baselines/analytics-growth.png b/e2e/visual-regression/baselines/analytics-growth.png new file mode 100644 index 0000000..5ea44d0 Binary files /dev/null and b/e2e/visual-regression/baselines/analytics-growth.png differ diff --git a/e2e/visual-regression/baselines/analytics-newsletters.png b/e2e/visual-regression/baselines/analytics-newsletters.png new file mode 100644 index 0000000..5ea44d0 Binary files /dev/null and b/e2e/visual-regression/baselines/analytics-newsletters.png differ diff --git a/e2e/visual-regression/baselines/analytics-overview.png b/e2e/visual-regression/baselines/analytics-overview.png new file mode 100644 index 0000000..5ea44d0 Binary files /dev/null and b/e2e/visual-regression/baselines/analytics-overview.png differ diff --git a/e2e/visual-regression/baselines/analytics-web.png b/e2e/visual-regression/baselines/analytics-web.png new file mode 100644 index 0000000..5ea44d0 Binary files /dev/null and b/e2e/visual-regression/baselines/analytics-web.png differ diff --git a/e2e/visual-regression/baselines/dashboard.png b/e2e/visual-regression/baselines/dashboard.png new file mode 100644 index 0000000..a7d4d7e Binary files /dev/null and b/e2e/visual-regression/baselines/dashboard.png differ diff --git a/e2e/visual-regression/baselines/editor-new-post.png b/e2e/visual-regression/baselines/editor-new-post.png new file mode 100644 index 0000000..337f2f4 Binary files /dev/null and b/e2e/visual-regression/baselines/editor-new-post.png differ diff --git a/e2e/visual-regression/baselines/members-activity.png b/e2e/visual-regression/baselines/members-activity.png new file mode 100644 index 0000000..0a44bf2 Binary files /dev/null and b/e2e/visual-regression/baselines/members-activity.png differ diff --git a/e2e/visual-regression/baselines/members-list.png b/e2e/visual-regression/baselines/members-list.png new file mode 100644 index 0000000..9f62634 Binary files /dev/null and b/e2e/visual-regression/baselines/members-list.png differ diff --git a/e2e/visual-regression/baselines/pages-list.png b/e2e/visual-regression/baselines/pages-list.png new file mode 100644 index 0000000..3439f7f Binary files /dev/null and b/e2e/visual-regression/baselines/pages-list.png differ diff --git a/e2e/visual-regression/baselines/posts-list.png b/e2e/visual-regression/baselines/posts-list.png new file mode 100644 index 0000000..d079915 Binary files /dev/null and b/e2e/visual-regression/baselines/posts-list.png differ diff --git a/e2e/visual-regression/baselines/settings-analytics.png b/e2e/visual-regression/baselines/settings-analytics.png new file mode 100644 index 0000000..60df47f Binary files /dev/null and b/e2e/visual-regression/baselines/settings-analytics.png differ diff --git a/e2e/visual-regression/baselines/settings-announcement-bar.png b/e2e/visual-regression/baselines/settings-announcement-bar.png new file mode 100644 index 0000000..a8357a3 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-announcement-bar.png differ diff --git a/e2e/visual-regression/baselines/settings-code-injection.png b/e2e/visual-regression/baselines/settings-code-injection.png new file mode 100644 index 0000000..c80751c Binary files /dev/null and b/e2e/visual-regression/baselines/settings-code-injection.png differ diff --git a/e2e/visual-regression/baselines/settings-default-recipients.png b/e2e/visual-regression/baselines/settings-default-recipients.png new file mode 100644 index 0000000..3ba9447 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-default-recipients.png differ diff --git a/e2e/visual-regression/baselines/settings-design.png b/e2e/visual-regression/baselines/settings-design.png new file mode 100644 index 0000000..9e1c8aa Binary files /dev/null and b/e2e/visual-regression/baselines/settings-design.png differ diff --git a/e2e/visual-regression/baselines/settings-embed-signup-form.png b/e2e/visual-regression/baselines/settings-embed-signup-form.png new file mode 100644 index 0000000..f4a21b2 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-embed-signup-form.png differ diff --git a/e2e/visual-regression/baselines/settings-enable-newsletters.png b/e2e/visual-regression/baselines/settings-enable-newsletters.png new file mode 100644 index 0000000..16e5103 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-enable-newsletters.png differ diff --git a/e2e/visual-regression/baselines/settings-history.png b/e2e/visual-regression/baselines/settings-history.png new file mode 100644 index 0000000..6190a61 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-history.png differ diff --git a/e2e/visual-regression/baselines/settings-integrations.png b/e2e/visual-regression/baselines/settings-integrations.png new file mode 100644 index 0000000..ae77cbc Binary files /dev/null and b/e2e/visual-regression/baselines/settings-integrations.png differ diff --git a/e2e/visual-regression/baselines/settings-labs.png b/e2e/visual-regression/baselines/settings-labs.png new file mode 100644 index 0000000..7770164 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-labs.png differ diff --git a/e2e/visual-regression/baselines/settings-mailgun.png b/e2e/visual-regression/baselines/settings-mailgun.png new file mode 100644 index 0000000..9a90c94 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-mailgun.png differ diff --git a/e2e/visual-regression/baselines/settings-migration.png b/e2e/visual-regression/baselines/settings-migration.png new file mode 100644 index 0000000..b1338b6 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-migration.png differ diff --git a/e2e/visual-regression/baselines/settings-navigation.png b/e2e/visual-regression/baselines/settings-navigation.png new file mode 100644 index 0000000..c264c42 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-navigation.png differ diff --git a/e2e/visual-regression/baselines/settings-newsletters.png b/e2e/visual-regression/baselines/settings-newsletters.png new file mode 100644 index 0000000..962c19b Binary files /dev/null and b/e2e/visual-regression/baselines/settings-newsletters.png differ diff --git a/e2e/visual-regression/baselines/settings-portal.png b/e2e/visual-regression/baselines/settings-portal.png new file mode 100644 index 0000000..f659f6f Binary files /dev/null and b/e2e/visual-regression/baselines/settings-portal.png differ diff --git a/e2e/visual-regression/baselines/settings-publication-language.png b/e2e/visual-regression/baselines/settings-publication-language.png new file mode 100644 index 0000000..45c0de4 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-publication-language.png differ diff --git a/e2e/visual-regression/baselines/settings-recommendations.png b/e2e/visual-regression/baselines/settings-recommendations.png new file mode 100644 index 0000000..fc3b2fe Binary files /dev/null and b/e2e/visual-regression/baselines/settings-recommendations.png differ diff --git a/e2e/visual-regression/baselines/settings-social-accounts.png b/e2e/visual-regression/baselines/settings-social-accounts.png new file mode 100644 index 0000000..cb13099 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-social-accounts.png differ diff --git a/e2e/visual-regression/baselines/settings-staff.png b/e2e/visual-regression/baselines/settings-staff.png new file mode 100644 index 0000000..7e7827e Binary files /dev/null and b/e2e/visual-regression/baselines/settings-staff.png differ diff --git a/e2e/visual-regression/baselines/settings-theme.png b/e2e/visual-regression/baselines/settings-theme.png new file mode 100644 index 0000000..9418bea Binary files /dev/null and b/e2e/visual-regression/baselines/settings-theme.png differ diff --git a/e2e/visual-regression/baselines/settings-tiers.png b/e2e/visual-regression/baselines/settings-tiers.png new file mode 100644 index 0000000..dd90b53 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-tiers.png differ diff --git a/e2e/visual-regression/baselines/settings-timezone.png b/e2e/visual-regression/baselines/settings-timezone.png new file mode 100644 index 0000000..38bd630 Binary files /dev/null and b/e2e/visual-regression/baselines/settings-timezone.png differ diff --git a/e2e/visual-regression/baselines/settings-title-description.png b/e2e/visual-regression/baselines/settings-title-description.png new file mode 100644 index 0000000..fc5571e Binary files /dev/null and b/e2e/visual-regression/baselines/settings-title-description.png differ diff --git a/e2e/visual-regression/baselines/settings.png b/e2e/visual-regression/baselines/settings.png new file mode 100644 index 0000000..4e14ac7 Binary files /dev/null and b/e2e/visual-regression/baselines/settings.png differ diff --git a/e2e/visual-regression/baselines/stats.png b/e2e/visual-regression/baselines/stats.png new file mode 100644 index 0000000..e942e2a Binary files /dev/null and b/e2e/visual-regression/baselines/stats.png differ diff --git a/e2e/visual-regression/baselines/tags-list.png b/e2e/visual-regression/baselines/tags-list.png new file mode 100644 index 0000000..ff7a95c Binary files /dev/null and b/e2e/visual-regression/baselines/tags-list.png differ diff --git a/e2e/visual-regression/baselines/tags-new.png b/e2e/visual-regression/baselines/tags-new.png new file mode 100644 index 0000000..e5d25ef Binary files /dev/null and b/e2e/visual-regression/baselines/tags-new.png differ diff --git a/e2e/visual-regression/capture-baselines.spec.ts b/e2e/visual-regression/capture-baselines.spec.ts new file mode 100644 index 0000000..a2af595 --- /dev/null +++ b/e2e/visual-regression/capture-baselines.spec.ts @@ -0,0 +1,241 @@ +import {expect, test} from '@playwright/test'; + +/** + * Visual regression baselines for Ghost Admin. + * + * Each test navigates to a Ghost Admin screen, hides dynamic content + * (timestamps, notifications, avatars, animations) so they don't cause + * false diffs, and captures a full-page screenshot. + * + * Usage: + * # Generate / update baselines (run on main branch BEFORE migration) + * npx playwright test -c e2e/visual-regression --update-snapshots + * + * # Compare against baselines (run AFTER migration changes) + * npx playwright test -c e2e/visual-regression + */ + +interface Screen { + name: string; + path: string; + /** Optional selector to wait for before screenshotting */ + waitFor?: string; + /** Extra time to wait after load (ms) — use sparingly */ + extraWait?: number; + /** If true, capture viewport-only screenshot instead of full page */ + viewportOnly?: boolean; +} + +// --------------------------------------------------------------------------- +// Full-page screens +// --------------------------------------------------------------------------- +const SCREENS: Screen[] = [ + // Core Ember pages (HIGH risk — CSS class collisions with .flex, .hidden, etc.) + {name: 'dashboard', path: '/ghost/#/dashboard'}, + {name: 'posts-list', path: '/ghost/#/posts'}, + {name: 'pages-list', path: '/ghost/#/pages'}, + {name: 'tags-list', path: '/ghost/#/tags'}, + {name: 'tags-new', path: '/ghost/#/tags/new', extraWait: 1000}, + {name: 'members-list', path: '/ghost/#/members'}, + {name: 'members-activity', path: '/ghost/#/members-activity', extraWait: 1000}, + + // Editor + {name: 'editor-new-post', path: '/ghost/#/editor/post', extraWait: 2000}, + + // Settings (full page — captures top portion) + {name: 'settings', path: '/ghost/#/settings'}, + + // Analytics / Stats pages (React) + {name: 'analytics-overview', path: '/ghost/#/stats', extraWait: 1500}, + {name: 'analytics-web', path: '/ghost/#/stats/web', extraWait: 1500}, + {name: 'analytics-growth', path: '/ghost/#/stats/growth', extraWait: 1500}, + {name: 'analytics-newsletters', path: '/ghost/#/stats/newsletters', extraWait: 1500}, + + // ActivityPub pages (React) + {name: 'activitypub-inbox', path: '/ghost/#/activitypub', extraWait: 1500}, + {name: 'activitypub-feed', path: '/ghost/#/activitypub/feed', extraWait: 1500}, + {name: 'activitypub-profile', path: '/ghost/#/activitypub/profile', extraWait: 1500}, + {name: 'activitypub-notifications', path: '/ghost/#/activitypub/notifications', extraWait: 1500} +]; + +// --------------------------------------------------------------------------- +// Settings sections — scrolled into view individually +// --------------------------------------------------------------------------- +interface SettingsSection { + name: string; + /** The data-testid attribute on the section's TopLevelGroup */ + testId: string; +} + +const SETTINGS_SECTIONS: SettingsSection[] = [ + // General + {name: 'settings-title-description', testId: 'title-and-description'}, + {name: 'settings-timezone', testId: 'timezone'}, + {name: 'settings-publication-language', testId: 'publication-language'}, + {name: 'settings-staff', testId: 'users'}, + {name: 'settings-social-accounts', testId: 'social-accounts'}, + + // Site + {name: 'settings-design', testId: 'design'}, + {name: 'settings-theme', testId: 'theme'}, + {name: 'settings-navigation', testId: 'navigation'}, + {name: 'settings-announcement-bar', testId: 'announcement-bar'}, + + // Membership + {name: 'settings-portal', testId: 'portal'}, + {name: 'settings-tiers', testId: 'tiers'}, + {name: 'settings-analytics', testId: 'analytics'}, + + // Email newsletters + {name: 'settings-enable-newsletters', testId: 'enable-newsletters'}, + {name: 'settings-newsletters', testId: 'newsletters'}, + {name: 'settings-default-recipients', testId: 'default-recipients'}, + {name: 'settings-mailgun', testId: 'mailgun'}, + + // Growth + {name: 'settings-recommendations', testId: 'recommendations'}, + {name: 'settings-embed-signup-form', testId: 'embed-signup-form'}, + + // Advanced + {name: 'settings-integrations', testId: 'integrations'}, + {name: 'settings-migration', testId: 'migrationtools'}, + {name: 'settings-code-injection', testId: 'code-injection'}, + {name: 'settings-labs', testId: 'labs'}, + {name: 'settings-history', testId: 'history'} +]; + +// --------------------------------------------------------------------------- +// CSS injected into every page to hide flaky dynamic content +// --------------------------------------------------------------------------- +const HIDE_DYNAMIC_CONTENT = ` + /* Timestamps and relative dates */ + [data-testid="timestamp"], + [data-test-date], + time, + .gh-content-entry-date, + .gh-members-list-joined, + .gh-post-list-updated, + .gh-post-list-date { + visibility: hidden !important; + } + + /* Notifications and toasts */ + .gh-notification, + .gh-alerts, + [data-testid="toast"] { + display: none !important; + } + + /* Animations and transitions */ + *, *::before, *::after { + animation-duration: 0s !important; + animation-delay: 0s !important; + transition-duration: 0s !important; + transition-delay: 0s !important; + } + + /* Avatars (can vary between runs) */ + .gh-member-avatar img, + .gh-author-avatar img { + visibility: hidden !important; + } + + /* Loading spinners */ + .gh-loading-spinner, + .gh-loading-orb { + display: none !important; + } + + /* Charts and graphs (data-dependent, flaky) */ + .recharts-surface, + .recharts-wrapper, + canvas { + visibility: hidden !important; + } + + /* Scrollbars (platform-dependent rendering) */ + ::-webkit-scrollbar { + display: none !important; + } + * { + scrollbar-width: none !important; + } + + /* Editor carets and cursors */ + .koenig-cursor, + .ProseMirror .ProseMirror-cursor, + [data-lexical-editor] .cursor, + .kg-prose caret-color { + caret-color: transparent !important; + } +`; + +// --------------------------------------------------------------------------- +// Full-page screen tests +// --------------------------------------------------------------------------- +for (const screen of SCREENS) { + test(`visual baseline: ${screen.name}`, async ({page}) => { + await page.goto(screen.path); + await page.waitForLoadState('load'); + + if (screen.waitFor) { + await page.waitForSelector(screen.waitFor, {timeout: 15_000}); + } + + if (screen.extraWait) { + await page.waitForTimeout(screen.extraWait); + } + + await page.addStyleTag({content: HIDE_DYNAMIC_CONTENT}); + await page.waitForTimeout(500); + + await expect(page).toHaveScreenshot(`${screen.name}.png`, { + fullPage: !screen.viewportOnly, + maxDiffPixelRatio: 0.001 + }); + }); +} + +// --------------------------------------------------------------------------- +// Settings section tests — scroll to each section and capture viewport +// --------------------------------------------------------------------------- +test.describe('settings sections', () => { + for (const section of SETTINGS_SECTIONS) { + test(`visual baseline: ${section.name}`, async ({page}) => { + await page.goto('/ghost/#/settings'); + await page.waitForLoadState('load'); + + // Wait for Settings React app to mount + await page.waitForSelector('[data-testid="title-and-description"]', {timeout: 15_000}); + + // Extra settle time for settings to fully render + await page.waitForTimeout(1500); + + await page.addStyleTag({content: HIDE_DYNAMIC_CONTENT}); + + // Scroll the section into view within the settings scroller + const scrolled = await page.evaluate((testId) => { + const sectionEl = document.querySelector(`[data-testid="${testId}"]`); + if (!sectionEl) { + return false; + } + sectionEl.scrollIntoView({block: 'start'}); + return true; + }, section.testId); + + if (!scrolled) { + // Section might not exist (conditional on config like Stripe) + test.skip(); + return; + } + + // Allow scroll + re-render to settle + await page.waitForTimeout(500); + + await expect(page).toHaveScreenshot(`${section.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.001 + }); + }); + } +}); diff --git a/e2e/visual-regression/playwright.config.ts b/e2e/visual-regression/playwright.config.ts new file mode 100644 index 0000000..d977d41 --- /dev/null +++ b/e2e/visual-regression/playwright.config.ts @@ -0,0 +1,50 @@ +import {defineConfig} from '@playwright/test'; + +/** + * Visual regression test config for TailwindCSS migration. + * + * Runs against a live `pnpm dev` Ghost instance (localhost:2368). + * Start Ghost first, then: + * + * # Capture/update golden baselines + * npx playwright test -c e2e/visual-regression --update-snapshots + * + * # Compare current state against baselines + * npx playwright test -c e2e/visual-regression + */ +export default defineConfig({ + testDir: './', + testMatch: '**/*.spec.ts', + timeout: 90_000, + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.001, + animations: 'disabled' + } + }, + retries: 0, + workers: 1, // sequential — screenshots must be deterministic + reporter: [['list', {printSteps: true}], ['html', {open: 'never'}]], + use: { + baseURL: process.env.GHOST_URL || 'http://localhost:2368', + viewport: {width: 1440, height: 900}, + actionTimeout: 10_000, + screenshot: 'only-on-failure', + trace: 'retain-on-failure' + }, + projects: [ + { + name: 'auth-setup', + testMatch: 'auth.setup.ts' + }, + { + name: 'visual-regression', + testMatch: 'capture-baselines.spec.ts', + dependencies: ['auth-setup'], + use: { + storageState: './e2e/visual-regression/.auth/state.json' + } + } + ], + snapshotPathTemplate: '{testDir}/baselines/{arg}{ext}' +}); diff --git a/ghost/admin/.editorconfig b/ghost/admin/.editorconfig new file mode 100644 index 0000000..32e8c92 --- /dev/null +++ b/ghost/admin/.editorconfig @@ -0,0 +1,26 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.hbs] +insert_final_newline = false + +[{package,bower}.json] +indent_size = 2 + +[*.{diff,md}] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/ghost/admin/.ember-cli b/ghost/admin/.ember-cli new file mode 100644 index 0000000..53f3969 --- /dev/null +++ b/ghost/admin/.ember-cli @@ -0,0 +1,11 @@ +{ + "component-structure": "flat", + "component-class": "@glimmer/component", + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": true +} diff --git a/ghost/admin/.eslintignore b/ghost/admin/.eslintignore new file mode 100644 index 0000000..3127254 --- /dev/null +++ b/ghost/admin/.eslintignore @@ -0,0 +1,20 @@ +# unconventional js +/blueprints/*/files/ +/vendor/ + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/coverage/ +.eslintcache + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/ghost/admin/.eslintrc.js b/ghost/admin/.eslintrc.js new file mode 100644 index 0000000..5ba6a44 --- /dev/null +++ b/ghost/admin/.eslintrc.js @@ -0,0 +1,70 @@ +/* eslint-env node */ +module.exports = { + root: true, + parser: '@babel/eslint-parser', + parserOptions: { + sourceType: 'module', + allowImportExportEverywhere: false, + ecmaFeatures: { + globalReturn: false, + legacyDecorators: true, + jsx: true + }, + requireConfigFile: false, + babelOptions: { + plugins: [ + '@babel/plugin-proposal-class-properties', + ['@babel/plugin-proposal-decorators', {legacy: true}], + 'babel-plugin-transform-react-jsx' + ] + } + }, + plugins: [ + 'ghost', + 'react' + ], + extends: [ + 'plugin:ghost/ember' + ], + rules: { + 'ghost/filenames/match-exported-class': ['off'], + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false], + 'no-shadow': ['error'], + + // TODO: migrate away from accessing controller in routes + 'ghost/ember/no-controller-access-in-routes': 'off', + + // TODO: enable once we're fully on octane 🏎 + 'ghost/ember/no-assignment-of-untracked-properties-used-in-tracking-contexts': 'off', + 'ghost/ember/no-actions-hash': 'off', + 'ghost/ember/no-classic-classes': 'off', + 'ghost/ember/no-classic-components': 'off', + 'ghost/ember/require-tagless-components': 'off', + 'ghost/ember/no-component-lifecycle-hooks': 'off', + + // disable linting of `this.get` until there's a reliable autofix + 'ghost/ember/use-ember-get-and-set': 'off', + + // disable linting of mixins until we migrate away + 'ghost/ember/no-mixins': 'off', + 'ghost/ember/no-new-mixins': 'off', + + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error' + }, + overrides: [{ + files: 'tests/**/*.js', + env: { + embertest: true, + mocha: true + }, + extends: [ + 'plugin:ghost/test' + ], + rules: { + 'ghost/ember/no-invalid-debug-function-arguments': 'off', + 'ghost/mocha/no-setup-in-describe': 'off' + } + }] +}; diff --git a/ghost/admin/.lint-todo b/ghost/admin/.lint-todo new file mode 100644 index 0000000..a8373aa --- /dev/null +++ b/ghost/admin/.lint-todo @@ -0,0 +1,169 @@ +add|ember-template-lint|no-action|5|11|5|11|8eaebb48eca1563c6e0b18581df84ab59188d971|1746489600000|||app/components/gh-cm-editor.hbs +add|ember-template-lint|no-passed-in-event-handlers|5|4|5|4|3a763e253744b070633bb8bd424b6c8e55f6b20a|1746489600000|||app/components/gh-cm-editor.hbs +add|ember-template-lint|no-invalid-interactive|1|103|1|103|534029ab0ba1b74eff4a2f31c8b4dd9f1460316a|1746489600000|||app/components/gh-context-menu.hbs +add|ember-template-lint|no-invalid-interactive|5|53|5|53|9647ef6afba919b2af04fe551b0fdf0fb63be849|1746489600000|||app/components/gh-context-menu.hbs +add|ember-template-lint|no-action|5|18|5|18|0c80a75b2a80d404755333991c266c81c97c9cda|1746489600000|||app/components/gh-date-time-picker.hbs +add|ember-template-lint|no-action|9|19|9|19|d12bcf1144bfb2fe70e7ab0f66836f1c6207a589|1746489600000|||app/components/gh-editor.hbs +add|ember-template-lint|no-action|10|20|10|20|16d94650de2ffbe8ee3f2ce3ba5ca97a6304b739|1746489600000|||app/components/gh-editor.hbs +add|ember-template-lint|no-action|11|17|11|17|30d64b1bf8990e2f84c52665690739fcc726e9f7|1746489600000|||app/components/gh-editor.hbs +add|ember-template-lint|no-action|2|45|2|45|55b33496610b2f4ef6b9fe62713f4b4ddee37f19|1746489600000|||app/components/gh-fullscreen-modal.hbs +add|ember-template-lint|no-action|10|29|10|29|ebbd89a393bcec7f537f2104ace2a6b1941a19a7|1746489600000|||app/components/gh-fullscreen-modal.hbs +add|ember-template-lint|no-action|11|32|11|32|ab89b6f10c519be1271386203e9439d261eecd67|1746489600000|||app/components/gh-fullscreen-modal.hbs +add|ember-template-lint|no-action|13|36|13|36|3776877637b49b65deef537c68ae490b2fb081a9|1746489600000|||app/components/gh-fullscreen-modal.hbs +add|ember-template-lint|no-invalid-interactive|2|45|2|45|918fdec8490009c6197091e319d6ec52ee95b983|1746489600000|||app/components/gh-fullscreen-modal.hbs +add|ember-template-lint|link-href-attributes|8|8|8|8|2078c6e43d1e5548ae745b7ac8cb736f7d2aaf68|1746489600000|||app/components/gh-image-uploader-with-preview.hbs +add|ember-template-lint|no-invalid-interactive|8|60|8|60|2078c6e43d1e5548ae745b7ac8cb736f7d2aaf68|1746489600000|||app/components/gh-image-uploader-with-preview.hbs +add|ember-template-lint|require-valid-alt-text|3|13|3|13|079fc89fa5c7c47f6b0b219820cdda3819c44e26|1746489600000|||app/components/gh-image-uploader-with-preview.hbs +add|ember-template-lint|no-action|12|58|12|58|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-action|17|75|17|75|bbc5d9a459cd07e56d8c79dde619775b89d7cc89|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-action|22|52|22|52|b1bd53a513ad82434d5a0a9a96441a45d416d7ad|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-action|31|16|31|16|4c64ddeaf795ee831d0d7346667a305814ca9855|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-action|32|15|32|15|b1bd53a513ad82434d5a0a9a96441a45d416d7ad|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-invalid-interactive|22|52|22|52|74dea739bef284d6557987ff3da53fa1278030e2|1746489600000|||app/components/gh-image-uploader.hbs +add|ember-template-lint|no-action|10|16|10|16|8d8dd8c2cb5f9910c2de5ca6acc76ee4262a876e|1746489600000|||app/components/gh-members-import-mapping-input.hbs +add|ember-template-lint|no-action|9|36|9|36|05358b6ec6e9afbaa47416266c49f40a3ffb4490|1746489600000|||app/components/gh-members-import-table.hbs +add|ember-template-lint|no-action|10|36|10|36|c5ce93cf577ec47970f715ed83ed11a63adb7a63|1746489600000|||app/components/gh-members-import-table.hbs +add|ember-template-lint|no-redundant-role|6|20|6|20|bc4fbabe3d468440d8a2c70e92cec991caca41e2|1746489600000|||app/components/gh-post-bookmark.hbs +add|ember-template-lint|no-redundant-role|14|64|14|64|ce988c0098c6e4845fc3307eb523b8b1687a3ecf|1746489600000|||app/components/gh-post-bookmark.hbs +add|ember-template-lint|no-action|36|43|36|43|66cebfc8448eced0bccf3faa57b4af0cd633e65e|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|37|47|37|47|70f3e9aa9a9aa52142c4e0c015a8f173d12b05ed|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|56|41|56|41|43cc094c1a6b50ecd24c8af325dfdfcac863bb14|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|57|41|57|41|e7f429eefd04a55d4ef1875fe601da1b69964364|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|87|50|87|50|0e827c1770073f988535ceb9d6c49808a153d896|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|104|39|104|39|5cbc9d2abf29e108bfc207579df1206485e2a74d|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|105|43|105|43|1f2dd4961e9757ede9706bb0aa9b8baf9c49b22b|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|116|132|116|132|90dbb84741c72aa86a601e1cb95c066bd53898bd|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|123|46|123|46|56ca074eb0236b8becc4084423056a8ffa4453e9|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|158|73|158|73|b07bf9d50af5f00861571521b150cf6504b90704|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|172|56|172|56|38755451c044c56040684339301c21b2aab49ecb|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|190|50|190|50|9f1c3c88187e5db774f97704269e372f8331eba5|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|196|50|196|50|97acfb2045b33a97621258596b631362ad4c56d5|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|202|51|202|51|358336432fcb413c522c5f1ff3062a68ef9f449f|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|208|50|208|50|78a45cbb7eafdda134d96f6035b0026f339e75ad|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|214|50|214|50|693211baf08c011e77284b483c30e28ecaf63520|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|223|105|223|105|de5b68b49193c72f85c0e478f151a00180321d91|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|236|167|236|167|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|601|163|601|163|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|607|34|607|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|616|47|616|47|67551960e57b2ad06d24289e0d6f256dc5635cae|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|617|51|617|51|50e3029f4c845049ff10130cf0dc2ea06c7a3d2d|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|632|47|632|47|0d852775b4028d61c2c91b5d821c79de3cdbd1de|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|633|51|633|51|194439ec4cb10e176eecb1f55c2995c00f6c67b4|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|647|47|647|47|3be93048908ab43d74726c3983982fcb9a3570e3|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|648|51|648|51|40871d259cc307345b8096e4215ae58e5886b724|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|674|166|674|166|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|681|34|681|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|686|44|686|44|d2abbb4bb55b6cb1131ca0bc56c31632b32b980c|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|687|44|687|44|3ede83bd64d206d665edb4f1b73f03b4122edfad|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|697|47|697|47|13b7064b520a924d9e57a4a680621ce979d23923|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|698|51|698|51|b0fc38b818f4ca2613684588e4122e12da0090b9|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|713|47|713|47|f34f084dc72c079be9ba3eb93c255bd8853612bd|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|714|51|714|51|c5c09d001fa96a623649cc88ef13b3c6c164f05b|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|742|167|742|167|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|748|34|748|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|753|44|753|44|32101c0834d9e8d2caa87dee9cb1a92fe633cfde|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|754|44|754|44|c8a82286cfe4220cb2d8a059b1daf7f6c8d54cf5|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|764|47|764|47|31800fb83f1797de5d91b7007c48432cf2fae803|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|765|51|765|51|5ce0788874489341385f031810e9f7f62724aaf8|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|778|47|778|47|1cbd7d922a9d202772a9dca214d97bed18777d6e|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|779|51|779|51|16fb2c7c87dc282c6ee49a032aaaf22628b88084|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|810|168|810|168|0145b67f0faef0aad141c6a4269c35c6ef8f0a47|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|816|34|816|34|3d44a2ad21d60d18bed6f79265caa4887361aaa4|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|823|50|823|50|d03e7bbf2b6de94b08fae1c33bd5a2f16874e03a|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|825|48|825|48|bca167f0250b40d56ff1ed40ec21bc88f8e27ec3|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|836|50|836|50|93ae447fb1054de4dbe501fc60167a6ff3273687|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|838|48|838|48|ecb9ed2577be7ea0300563f22d8e9fde2fed12a6|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|36|36|36|36|fed655605208b290a18b9f7da51a6cf7b40c0e9a|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|104|32|104|32|187e85e14470b453a1e6df8c5f18549d11c441a0|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|616|40|616|40|f290a04fd56f613d80d61244d161631f630185a8|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|632|40|632|40|c1af88109f705acc93fab4ee7b4d89096136ffe1|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|647|40|647|40|852cbb2c63e19a28d700d3b18e6f887edef303fa|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|697|40|697|40|58185c92e8b6261ea1483a70296d9fa837d3f2f5|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|713|40|713|40|c4353acd715c396564c69389edd0a52246d3b966|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|764|40|764|40|29f26e46559dc40d9724a05b7516cb52f1481aaa|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|778|40|778|40|42e0617f832585eaa056c9c66dffe1a126318faf|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|823|40|823|40|d1dccfeee5b103fac3b14d5b4567bc65ee08fd5a|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-passed-in-event-handlers|836|40|836|40|055c4b70aa8daba6b97077eefa95ebed7f7f5315|1746489600000|||app/components/gh-post-settings-menu.hbs +add|ember-template-lint|no-action|4|14|4|14|c8f6146f9286ec1b289e28983ab446386f390f01|1746489600000|||app/components/gh-psm-authors-input.hbs +add|ember-template-lint|no-action|9|24|9|24|2e612be5e2b8e0b792f951c3c75b2f71df880365|1746489600000|||app/components/gh-psm-template-select.hbs +add|ember-template-lint|no-action|7|16|7|16|86819b20fcc42bc1d4607519c1d45532ee337884|1746489600000|||app/components/gh-psm-visibility-input.hbs +add|ember-template-lint|no-autofocus-attribute|13|16|13|16|fa0ffb960072633b72117849e3927673be0059af|1746489600000|||app/components/gh-search-input.hbs +add|ember-template-lint|require-iframe-title|1|0|1|0|956ab219134ac63aec3fd2d35582076c21631b75|1746489600000|||app/components/gh-site-iframe.hbs +add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1746489600000|||app/components/gh-text-input.hbs +add|ember-template-lint|no-action|1|51|1|51|674a942db62e014b25156b02d014f6c63b2f1cb2|1746489600000|||app/components/gh-theme-error-li.hbs +add|ember-template-lint|no-action|2|11|2|11|2b1317e72b94ec31bf2700acc3f041e6be974b72|1746489600000|||app/components/gh-uploader.hbs +add|ember-template-lint|no-action|7|13|7|13|add4b57d48167f809045245535d45bedb00cb753|1746489600000|||app/components/gh-uploader.hbs +add|ember-template-lint|no-action|8|22|8|22|ce1eba2b791c0f120baf10871fd3949c1b9f6a79|1746489600000|||app/components/gh-uploader.hbs +add|ember-template-lint|no-action|9|22|9|22|0209f233628ec084b87894b4be9073c29f2a4f47|1746489600000|||app/components/gh-uploader.hbs +add|ember-template-lint|no-passed-in-event-handlers|4|4|4|4|3dedc53191768d81765eac4a7a049a9aba7df442|1746489600000|||app/components/gh-url-input.hbs +add|ember-template-lint|no-action|1|71|1|71|2e6351f546807d88cc8eb9dbe8baa149468b5cb9|1746489600000|||app/components/gh-view-title.hbs +add|ember-template-lint|no-invalid-role|1|0|1|0|3e651d38e0110e1be20e5082075db1b879b59a36|1746489600000|||app/components/gh-view-title.hbs +add|ember-template-lint|no-action|5|50|5|50|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-impersonate-member.hbs +add|ember-template-lint|no-action|5|74|5|74|d465b362b15b90cf42a093e72895155f49cdf6f2|1746489600000|||app/components/modal-impersonate-member.hbs +add|ember-template-lint|no-action|43|57|43|57|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|50|56|50|56|190532b9954beb4e7e9e0f1c51d170e64d4a5315|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|56|34|56|34|ff609af61e9159dfc5386543d559f6d217d39531|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|57|29|57|29|9a0a72738b6e1a4f46415b2328c37d1814562717|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|110|89|110|89|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|116|91|116|91|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|119|116|119|116|7e581bf2ffd5254ae851201e1f23cb9eaa9b198b|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|129|120|129|120|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|132|103|132|103|7e581bf2ffd5254ae851201e1f23cb9eaa9b198b|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|143|112|143|112|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|147|110|147|110|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|153|97|153|97|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|156|112|156|112|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|160|99|160|99|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|163|110|163|110|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|171|91|171|91|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|174|102|174|102|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|181|95|181|95|031d04576149d3034549aa3d14bc26da704cd70c|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-action|185|102|185|102|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-import-members.hbs +add|ember-template-lint|no-invalid-interactive|28|24|28|24|42a29ae16e22270f0590c9ce5caa7bfec541ca0b|1746489600000|||app/components/modal-member-tier.hbs +add|ember-template-lint|no-action|5|57|5|57|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|14|45|14|45|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|23|59|23|59|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|23|83|23|83|d465b362b15b90cf42a093e72895155f49cdf6f2|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|34|31|34|31|a8ad062b8379233b7970fe5ea74296fdf5011567|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|47|82|47|82|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|49|16|49|16|d465b362b15b90cf42a093e72895155f49cdf6f2|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-action|55|113|55|113|c259009ff744c2e9f7bb273e0eb3a3879036ba85|1746489600000|||app/components/modal-members-label-form.hbs +add|ember-template-lint|no-redundant-role|85|24|85|24|ce988c0098c6e4845fc3307eb523b8b1687a3ecf|1746489600000|||app/components/modal-post-success.hbs +add|ember-template-lint|no-redundant-role|136|24|136|24|ce988c0098c6e4845fc3307eb523b8b1687a3ecf|1746489600000|||app/components/modal-post-success.hbs +add|ember-template-lint|no-action|4|53|4|53|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-unsubscribe-members.hbs +add|ember-template-lint|no-action|50|89|50|89|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-unsubscribe-members.hbs +add|ember-template-lint|no-action|54|71|54|71|141d456b03124abca146e58e4ae15825fdd040bb|1746489600000|||app/components/modal-unsubscribe-members.hbs +add|ember-template-lint|require-valid-alt-text|4|12|4|12|8369d1b06deac93e8c8e05444670c15182aea434|1746489600000|||app/templates/application-error.hbs +add|ember-template-lint|no-redundant-role|91|40|91|40|9d2eded16257516b455504637aab4057b3c0c9ef|1746489600000|||app/templates/mentions.hbs +add|ember-template-lint|no-redundant-role|113|20|113|20|0418319e2dce98b674dbb6657d185f465d21f87d|1746489600000|||app/templates/mentions.hbs +add|ember-template-lint|no-action|20|31|20|31|3aa834e53af871a821ebb1b47f7a04f925c2b593|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|21|35|21|35|ae65e93ed2b79a307e0eba50b154fe84ee1ae210|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|37|31|37|31|eefdee43411bdd45c2786b9e826e6b550fd6212d|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|38|35|38|35|a8e4bd4c57a8f2df65749b0cfa4349da22b2a0aa|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|55|31|55|31|1709109776bf3fc47aaecc21e6d3ec8a0489ad6b|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|56|35|56|35|8000241a81189d3876d264331ec0c9b6476df076|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|73|31|73|31|6756e119daad4aa143724e9b56a6aef744d65251|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|74|35|74|35|26b1d89c0e5bbcd629a215903c0819d35f798aa6|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-passed-in-event-handlers|20|24|20|24|f0b7babba7593639d68dadae2f19e7144dd8c63e|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-passed-in-event-handlers|37|24|37|24|2692530760cb1f7156dcf9570aacf0f8baca2770|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-passed-in-event-handlers|55|24|55|24|ce0d7e2e732b22ce3643fb9b1ac6ecef07c275ff|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-passed-in-event-handlers|73|24|73|24|80681fcec2258c3d81a231bbd635aa1f03ff1452|1746489600000|||app/templates/setup.hbs +add|ember-template-lint|no-action|6|108|6|108|ccc38f66549f9baedaa3b9943ae6634ea8f99e69|1746489600000|||app/templates/tags.hbs +add|ember-template-lint|no-action|7|110|7|110|c3819ce2b6989e8596be570ed0c9fb82b5012521|1746489600000|||app/templates/tags.hbs +add|ember-template-lint|require-valid-alt-text|15|32|15|32|80c1ce6724481312363dc4e1db42bf28b41909f2|1746489600000|||app/templates/whatsnew.hbs +add|ember-template-lint|no-invalid-interactive|8|51|8|51|66a27dbed218d15e49e91c72a93215ad0d90f778|1746489600000|||app/components/gh-power-select/trigger.hbs +add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1746489600000|||app/components/gh-token-input/label-token.hbs +add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1746489600000|||app/components/gh-token-input/tag-token.hbs +add|ember-template-lint|no-action|8|19|8|19|73ac7d3892fcbcf15c3d5c44fca14dd21016daea|1746489600000|||app/components/gh-token-input/trigger.hbs +add|ember-template-lint|no-positive-tabindex|46|8|46|8|6118264a9a0599fab6ad4da7264c1bfffa88687e|1746489600000|||app/components/gh-token-input/trigger.hbs +add|ember-template-lint|require-iframe-title|27|20|27|20|94e58d11848d5613900c2188ba63dac41f4c03bb|1746489600000|||app/components/modals/email-preview.hbs +add|ember-template-lint|require-iframe-title|42|16|42|16|a3292b469dc37f2f4791e7f224b0b65c8ecf5d18|1746489600000|||app/components/modals/email-preview.hbs +add|ember-template-lint|no-autofocus-attribute|21|20|21|20|942419d05c04ded6716f09faecd6b1ab55418121|1746489600000|||app/components/modals/new-custom-integration.hbs +add|ember-template-lint|no-invalid-interactive|2|37|2|37|e21ba31f54b631a428c28a1c9f88d0dc66f2f5fc|1746489600000|||app/components/modals/search.hbs +add|ember-template-lint|no-redundant-role|11|20|11|20|bc4fbabe3d468440d8a2c70e92cec991caca41e2|1746489600000|||app/components/dashboard/onboarding/share-modal.hbs +remove|ember-template-lint|no-action|6|108|6|108|ccc38f66549f9baedaa3b9943ae6634ea8f99e69|1746489600000|||app/templates/tags.hbs +remove|ember-template-lint|no-action|7|110|7|110|c3819ce2b6989e8596be570ed0c9fb82b5012521|1746489600000|||app/templates/tags.hbs +remove|ember-template-lint|require-valid-alt-text|4|12|4|12|8369d1b06deac93e8c8e05444670c15182aea434|1746489600000|||app/templates/application-error.hbs +remove|ember-template-lint|no-action|1|71|1|71|2e6351f546807d88cc8eb9dbe8baa149468b5cb9|1746489600000|||app/components/gh-view-title.hbs +remove|ember-template-lint|no-invalid-role|1|0|1|0|3e651d38e0110e1be20e5082075db1b879b59a36|1746489600000|||app/components/gh-view-title.hbs +add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1771200000000|||app/components/gh-view-title.hbs diff --git a/ghost/admin/.lint-todorc.js b/ghost/admin/.lint-todorc.js new file mode 100644 index 0000000..3c28483 --- /dev/null +++ b/ghost/admin/.lint-todorc.js @@ -0,0 +1,5 @@ +module.exports = { + 'ember-template-lint': { + daysToDecay: null + } +}; diff --git a/ghost/admin/.template-lintrc.js b/ghost/admin/.template-lintrc.js new file mode 100644 index 0000000..d464622 --- /dev/null +++ b/ghost/admin/.template-lintrc.js @@ -0,0 +1,12 @@ +module.exports = { + extends: "recommended", + + rules: { + 'no-forbidden-elements': ['meta', 'html', 'script'], + 'no-implicit-this': {allow: ['noop', 'now', 'site-icon-style']}, + 'no-inline-styles': false, + 'no-duplicate-landmark-elements': false, + 'no-pointer-down-event-binding': false, + 'no-triple-curlies': false + } +}; diff --git a/ghost/admin/.watchmanconfig b/ghost/admin/.watchmanconfig new file mode 100644 index 0000000..e7834e3 --- /dev/null +++ b/ghost/admin/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/ghost/admin/README.md b/ghost/admin/README.md new file mode 100644 index 0000000..95939f7 --- /dev/null +++ b/ghost/admin/README.md @@ -0,0 +1,59 @@ +# Ghost-Admin + +This is the home of the Ember.js-based Admin app that ships with [Ghost](https://github.com/tryghost/ghost). + +## Test + +### Running tests in the browser + +Run all tests in the browser by running `pnpm dev` in the Ghost monorepo and visiting http://localhost:4200/tests. The code is hotloaded on change and you can filter which tests to run. + +[Testing public documentation](https://ghost.notion.site/Testing-Ember-560cec6700fc4d37a58b3ba9febb4b4b) + +--- + +Tip: You can use `this.timeout(0); await this.pauseTest();` in your tests to temporarily pause the execution of browser tests. Use the browser console to inspect and debug the DOM, then resume tests by running `resumeTest()` directly in the browser console ([docs](https://guides.emberjs.com/v3.28.0/testing/testing-application/#toc_debugging-your-tests)) + + +### Running tests in the CLI + +To build and run tests in the CLI, you can use: + +```bash +TZ=UTC pnpm test +``` +_Note the `TZ=UTC` environment variable which is currently required to get tests working if your system timezone doesn't match UTC._ + +--- + +However, this is very slow when writing tests, as it requires the app to be rebuilt on every change. Instead, create a separate watching build with: + +```bash +pnpm build --environment=test -w -o="dist-test" +``` + +Then run tests with: + +```bash +TZ=UTC pnpm test 1 --reporter dot --path="dist-test" +``` + +The `--reporter dot` shows a dot (`.`) for every successful test, and `F` for every failed test. It renders the output of the failed tests only. + +--- + +To run a specific test file: +```bash +TZ=UTC pnpm test 1 --reporter dot --path="dist-test" -mp=tests/unit/helpers/gh-count-characters-test.js +``` + +--- + +To have a full list of the available options, run +```bash +ember exam --help +``` + +# Copyright & License + +Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE). Ghost and the Ghost Logo are trademarks of Ghost Foundation Ltd. Please see our [trademark policy](https://ghost.org/trademark/) for info on acceptable usage. diff --git a/ghost/admin/app/README.md b/ghost/admin/app/README.md new file mode 100644 index 0000000..535aefb --- /dev/null +++ b/ghost/admin/app/README.md @@ -0,0 +1,30 @@ +# Ghost Admin App + +Ember.js application used as a client-side admin for the [Ghost](http://ghost.org) blogging platform. This readme is a work in progress guide aimed at explaining the specific nuances of the Ghost Ember app to contributors whose main focus is on this side of things. + + +## CSS + +We use pure CSS, which is pre-processed for backwards compatibility by [Myth](http://myth.io). We do not follow any strict CSS framework, however our general style is pretty similar to BEM. + +Styles are primarily broken up into 4 main categories: + +* **Patterns** - are base level visual styles for HTML elements (eg. Buttons) +* **Components** - are groups of patterns used to create a UI component (eg. Modals) +* **Layouts** - are groups of components used to create application screens (eg. Settings) + +All of these separate files are subsequently imported and compiled in `app.css`. + + +## Front End Standards + +* 4 spaces for HTML & CSS indentation. Never tabs. +* Double quotes only, never single quotes. +* Use tags and elements appropriate for an HTML5 doctype (including self-closing tags) +* Adhere to the [Recess CSS](http://markdotto.com/2011/11/29/css-property-order/) property order. +* Always a space after a property's colon (.e.g, display: block; and not display:block;). +* End all lines with a semi-colon. +* For multiple, comma-separated selectors, place each selector on its own line. +* Use js- prefixed classes for JavaScript hooks into the DOM, and never use these in CSS as per [Slightly Obtrusive JavaScript](http://ozmm.org/posts/slightly_obtrusive_javascript.html) +* Avoid over-nesting CSS. Never nest more than 3 levels deep. +* Use comments to explain "why" not "what" (Good: This requires a z-index in order to appear above mobile navigation. Bad: This is a thing which is always on top!) diff --git a/ghost/admin/app/app.js b/ghost/admin/app/app.js new file mode 100755 index 0000000..909af6a --- /dev/null +++ b/ghost/admin/app/app.js @@ -0,0 +1,54 @@ +import 'ghost-admin/utils/link-component'; +import 'ghost-admin/utils/route'; +import Application from '@ember/application'; +import Resolver from 'ember-resolver'; +import config from 'ghost-admin/config/environment'; +import loadInitializers from 'ember-load-initializers'; +import moment from 'moment-timezone'; +import {registerWarnHandler} from '@ember/debug'; + +moment.updateLocale('en', { + relativeTime: { + m: '1 minute' + } +}); + +const rootElement = document.getElementById('ember-app'); + +const App = Application.extend({ + Resolver, + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + + // eslint-disable-next-line + customEvents: { + touchstart: null, + touchmove: null, + touchend: null, + touchcancel: null + }, + + ...(rootElement ? { + rootElement: '#ember-app' + } : {}) +}); + +// TODO: remove once the validations refactor is complete +// eslint-disable-next-line +registerWarnHandler((message, options, next) => { + let skip = [ + 'ds.errors.add', + 'ds.errors.remove', + 'ds.errors.clear' + ]; + + if (skip.includes(options.id)) { + return; + } + + next(message, options); +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/ghost/admin/app/index.html b/ghost/admin/app/index.html new file mode 100644 index 0000000..2317df1 --- /dev/null +++ b/ghost/admin/app/index.html @@ -0,0 +1,52 @@ + + + + + + Ghost + + {{content-for "head"}} + + + + + + + + + + + + + + + + + + + + {{content-for "head-footer"}} + + + +
+
+ +
+
+ + {{content-for "body"}} + + {{content-for "body-footer"}} + + + + + diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js new file mode 100644 index 0000000..8a5a79d --- /dev/null +++ b/ghost/admin/app/router.js @@ -0,0 +1,70 @@ +import EmberRouter from '@ember/routing/router'; +import config from 'ghost-admin/config/environment'; +import ghostPaths from 'ghost-admin/utils/ghost-paths'; + +const Router = EmberRouter.extend({ + location: config.locationType, // use HTML5 History API instead of hash-tag based URLs + rootURL: ghostPaths().adminRoot // admin interface lives under sub-directory /ghost +}); + +// eslint-disable-next-line array-callback-return +Router.map(function () { + this.route('home', {path: '/'}); + + this.route('setup'); + this.route('setup.done', {path: '/setup/done'}); + + this.route('signin'); + this.route('signin-verify', {path: '/signin/verify'}); + this.route('signout'); + this.route('signup', {path: '/signup/:token'}); + this.route('reset', {path: '/reset/:token'}); + + this.route('site'); + this.route('dashboard'); + this.route('launch'); + + this.route('pro', function () { + this.route('pro-sub', {path: '/*sub'}); + }); + + this.route('posts'); + this.route('posts.debug', {path: '/posts/analytics/:post_id/debug'}); + this.route('restore-posts', {path: '/restore'}); + + this.route('pages'); + + this.route('lexical-editor', {path: 'editor'}, function () { + this.route('new', {path: ':type'}); + this.route('edit', {path: ':type/:post_id'}); + }); + + this.route('tag.new', {path: '/tags/new'}); + this.route('tag', {path: '/tags/:tag_slug'}); + + this.route('explore', function () { + // actual Ember route, not rendered in iframe + this.route('connect'); + // iframe sub pages, used for categories + this.route('explore-sub', {path: '/*sub'}, function () { + // needed to allow search to work, as it uses URL + // params for search queries. They don't need to + // be visible, but may not be cut off. + this.route('explore-query', {path: '/*query'}); + }); + }); + + this.route('migrate', function () { + this.route('migrate', {path: '/*platform'}); + }); + + this.route('member.new', {path: '/members/new'}); + this.route('member', {path: '/members/:member_id'}); + this.route('members-activity'); + + this.route('react-fallback', {path: '/*path'}); + + this.route('designsandbox'); +}); + +export default Router; diff --git a/ghost/admin/app/transitions.js b/ghost/admin/app/transitions.js new file mode 100644 index 0000000..e944c4a --- /dev/null +++ b/ghost/admin/app/transitions.js @@ -0,0 +1,21 @@ +export default function () { + this.transition( + this.hasClass('fullscreen-modal-container'), + this.toValue(true), + this.use('fade', {duration: 150}), + this.reverse('fade', {duration: 150}) + ); + + this.transition( + this.hasClass('fade-transition'), + this.use('crossFade', {duration: 100}) + ); + + // TODO: Maybe animate with explode. gh-unsplash-window should ideally slide in from bottom to top of screen + // this.transition( + // this.hasClass('gh-unsplash-window'), + // this.toValue(true), + // this.use('toUp', {duration: 500}), + // this.reverse('toDown', {duration: 500}) + // ); +} diff --git a/ghost/admin/config/deprecation-workflow.js b/ghost/admin/config/deprecation-workflow.js new file mode 100644 index 0000000..cb6fee5 --- /dev/null +++ b/ghost/admin/config/deprecation-workflow.js @@ -0,0 +1,11 @@ +self.deprecationWorkflow = self.deprecationWorkflow || {}; +self.deprecationWorkflow.config = { + workflow: [ + // remove once ember-drag-drop removes usage of Component#sendAction + // https://github.com/mharris717/ember-drag-drop/issues/155 + {handler: 'silence', matchId: 'ember-component.send-action'}, + + // remove once liquid-fire and liquid-wormhole remove uses of `this.$()` + {handler: 'silence', matchId: 'ember-views.curly-components.jquery-element'} + ] +}; diff --git a/ghost/admin/config/environment.js b/ghost/admin/config/environment.js new file mode 100644 index 0000000..beb9bba --- /dev/null +++ b/ghost/admin/config/environment.js @@ -0,0 +1,82 @@ +/* eslint-env node */ +'use strict'; + +module.exports = function (environment) { + let ENV = { + modulePrefix: 'ghost-admin', + environment, + editorUrl: process.env.EDITOR_URL || '', + rootURL: '', + locationType: 'trailing-hash', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + // @TODO verify that String/Function need to be enabled + EXTEND_PROTOTYPES: { + Date: false, + Array: true, + String: true, + Function: false + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + + // override the default version string which contains git info from + // https://github.com/cibernox/git-repo-version. Only include the + // `major.minor` version numbers + version: require('../package.json').version.match(/^(\d+\.)?(\d+)/)[0] + }, + + 'ember-simple-auth': { }, + + '@sentry/ember': { + disablePerformance: true, + sentry: {} + } + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + ENV.APP.LOG_ACTIVE_GENERATION = true; + ENV.APP.LOG_TRANSITIONS = true; + ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + ENV.APP.LOG_VIEW_LOOKUPS = true; + + // Enable mirage here in order to mock API endpoints during development + ENV['ember-cli-mirage'] = { + enabled: false + }; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.rootURL = '/'; + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + + // Without manually setting this, pretender won't track requests + ENV['ember-cli-mirage'] = { + trackRequests: true + }; + + // We copy the dynamically loaded editor file into the ghost assets + // directory in the dev/test env so that tests can load it. We need to + // set the config appropriately here so that the fetchKoenigLexical + // utility creates the right URL + ENV.editorFilename = 'koenig-lexical.umd.js'; + ENV.editorHash = 'test'; + } + + return ENV; +}; diff --git a/ghost/admin/config/optional-features.json b/ghost/admin/config/optional-features.json new file mode 100644 index 0000000..062be1d --- /dev/null +++ b/ghost/admin/config/optional-features.json @@ -0,0 +1,5 @@ +{ + "application-template-wrapper": false, + "jquery-integration": true, + "template-only-glimmer-components": true +} diff --git a/ghost/admin/config/targets.js b/ghost/admin/config/targets.js new file mode 100644 index 0000000..b2fb9d1 --- /dev/null +++ b/ghost/admin/config/targets.js @@ -0,0 +1,12 @@ +/* eslint-env node */ + +const browsers = [ + 'last 2 Chrome versions', + 'last 2 Firefox versions', + 'last 3 Safari versions', + 'last 2 Edge versions' +]; + +module.exports = { + browsers +}; diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js new file mode 100644 index 0000000..a50c972 --- /dev/null +++ b/ghost/admin/ember-cli-build.js @@ -0,0 +1,275 @@ +/* eslint-env node */ +'use strict'; + +// Check Node.js version compatibility before building +require('./lib/check-node-version')(); + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); +const concat = require('broccoli-concat'); +const mergeTrees = require('broccoli-merge-trees'); +const Terser = require('broccoli-terser-sourcemap'); +const Funnel = require('broccoli-funnel'); +const webpack = require('webpack'); +const environment = EmberApp.env(); +const isDevelopment = environment === 'development'; +const isProduction = environment === 'production'; +const isTesting = environment === 'test'; + +const postcssImport = require('postcss-import'); +const postcssCustomProperties = require('postcss-custom-properties'); +const postcssColorModFunction = require('postcss-color-mod-function'); +const postcssCustomMedia = require('postcss-custom-media'); +const autoprefixer = require('autoprefixer'); +const cssnano = require('cssnano'); + +const codemirrorAssets = function () { + let codemirrorFiles = [ + 'lib/codemirror.js', + 'mode/htmlmixed/htmlmixed.js', + 'mode/xml/xml.js', + 'mode/css/css.js', + 'mode/javascript/javascript.js' + ]; + + if (environment === 'test') { + return {import: codemirrorFiles}; + } + + let config = {}; + + config.public = { + include: codemirrorFiles, + destDir: '/', + processTree(tree) { + let jsTree = concat(tree, { + outputFile: 'assets/codemirror/codemirror.js', + headerFiles: ['lib/codemirror.js'], + inputFiles: ['mode/**/*'], + sourceMapConfig: {enabled: false} + }); + + if (isProduction) { + jsTree = new Terser(jsTree); + } + + let mergedTree = mergeTrees([tree, jsTree]); + return new Funnel(mergedTree, {include: ['assets/**/*', 'theme/**/*']}); + } + }; + + // put the files in vendor ready for importing into the test-support file + if (environment === 'development') { + config.vendor = codemirrorFiles; + } + + return config; +}; + +let denylist = []; +if (process.env.CI) { + denylist.push('ember-cli-eslint'); +} + +let publicAssetURL; + +if (isTesting) { + publicAssetURL = undefined; +} else if (process.env.GHOST_CDN_URL) { + publicAssetURL = process.env.GHOST_CDN_URL + 'assets/'; +} else { + publicAssetURL = 'assets/'; +} + +module.exports = function (defaults) { + let app = new EmberApp(defaults, { + addons: {denylist}, + babel: { + plugins: [ + require.resolve('babel-plugin-transform-react-jsx') + ] + }, + 'ember-cli-babel': { + optional: ['es6.spec.symbols'], + includePolyfill: false + }, + 'ember-composable-helpers': { + only: ['join', 'optional', 'pick', 'toggle', 'toggle-action', 'compute'] + }, + 'ember-promise-modals': { + excludeCSS: true + }, + outputPaths: { + app: { + js: 'assets/ghost.js', + css: { + app: 'assets/ghost.css', + // TODO: find a way to use the .min file with the lazyLoader + 'app-dark': 'assets/ghost-dark.css' + } + } + }, + fingerprint: { + enabled: isProduction, + extensions: [ + 'js', + 'css', + 'png', + 'jpg', + 'jpeg', + 'gif', + 'map', + 'svg', + 'ttf', + 'woff2', + 'mp4', + 'ico' + ], + exclude: ['**/chunk*.map'] + }, + minifyJS: { + options: { + output: { + semicolons: true + } + } + }, + minifyCSS: { + // postcss already handles minification and this was stripping required CSS + enabled: false + }, + nodeAssets: { + codemirror: codemirrorAssets() + }, + postcssOptions: { + compile: { + enabled: true, + plugins: [ + { + module: postcssImport, + options: { + path: ['app/styles'] + } + }, + { + module: postcssCustomProperties, + options: { + preserve: false + } + }, + { + module: postcssColorModFunction + }, + { + module: postcssCustomMedia + }, + { + module: autoprefixer + }, + { + module: cssnano, + options: { + zindex: false, + // cssnano sometimes minifies animations incorrectly causing them to break + // See: https://github.com/ben-eb/gulp-cssnano/issues/33#issuecomment-210518957 + reduceIdents: { + keyframes: false + }, + discardUnused: { + keyframes: false + } + } + } + ] + } + }, + sourcemaps: {enabled: true}, + svgJar: { + strategy: 'inline', + stripPath: false, + sourceDirs: [ + 'public/assets/icons' + ], + optimizer: { + plugins: [ + {prefixIds: true}, + {cleanupIds: false}, + {removeDimensions: true}, + {removeTitle: !isDevelopment}, + {removeXMLNS: true}, + // Transforms on groups are necessary to work around Firefox + // not supporting transform-origin on line/path elements. + {convertPathData: { + applyTransforms: false + }}, + {moveGroupAttrsToElems: false} + ] + } + }, + autoImport: { + publicAssetURL, + alias: { + 'sentry-testkit/browser': 'sentry-testkit/dist/browser' + }, + webpack: { + devtool: 'source-map', + resolve: { + fallback: { + util: require.resolve('util'), + path: require.resolve('path-browserify'), + fs: false + } + }, + ...(isDevelopment && { + cache: { + type: 'filesystem', + buildDependencies: { + config: [__filename] + } + } + }), + plugins: [ + new webpack.ProvidePlugin({ + process: 'process/browser' + }) + ], + // disable verbose logging about webpack resolution mismatches + // - this is a known issue with mismatched versions of dependencies resulting in duplication rather than a single hoisted version + // - we don't plan on fixing this in the short term, so we just silence the noise + infrastructureLogging: { + level: 'error' + } + } + }, + 'ember-test-selectors': { + strip: false + } + }); + + // Stop: Normalize + app.import('node_modules/normalize.css/normalize.css'); + + // 'dem Styles + // import codemirror styles rather than lazy-loading so that + // our overrides work correctly + app.import('node_modules/codemirror/lib/codemirror.css'); + app.import('node_modules/codemirror/theme/xq-light.css'); + + // 'dem Scripts + app.import('node_modules/google-caja-bower/html-css-sanitizer-bundle.js'); + app.import('node_modules/keymaster/keymaster.js'); + app.import('node_modules/reframe.js/dist/noframe.js'); + + // pull things we rely on via lazy-loading into the test-support.js file so + // that tests don't break when running via http://localhost:4200/tests + if (app.env === 'development') { + app.import('vendor/codemirror/lib/codemirror.js', {type: 'test'}); + } + + if (app.env === 'development' || app.env === 'test') { + // pull dynamic imports into the assets folder so that they can be lazy-loaded in tests + // also done in development env so http://localhost:4200/tests works + app.import('node_modules/@tryghost/koenig-lexical/dist/koenig-lexical.umd.js', {outputFile: 'ghost/assets/koenig-lexical/koenig-lexical.umd.js'}); + } + + return app.toTree(); +}; diff --git a/ghost/admin/ember-cli-update.json b/ghost/admin/ember-cli-update.json new file mode 100644 index 0000000..bbc5987 --- /dev/null +++ b/ghost/admin/ember-cli-update.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": "1.0.0", + "packages": [ + { + "name": "ember-cli", + "version": "3.20.0", + "blueprints": [ + { + "name": "app", + "outputRepo": "https://github.com/ember-cli/ember-new-output", + "codemodsSource": "ember-app-codemods-manifest@1", + "isBaseBlueprint": true, + "options": [] + } + ] + } + ] +} diff --git a/ghost/admin/jsconfig.json b/ghost/admin/jsconfig.json new file mode 100644 index 0000000..f408cac --- /dev/null +++ b/ghost/admin/jsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} \ No newline at end of file diff --git a/ghost/admin/lib/check-node-version.js b/ghost/admin/lib/check-node-version.js new file mode 100644 index 0000000..267c173 --- /dev/null +++ b/ghost/admin/lib/check-node-version.js @@ -0,0 +1,53 @@ +/* eslint-env node */ +'use strict'; + +const chalk = require('chalk'); +const semver = require('semver'); + +/** + * Check Node.js version compatibility for Ember admin build + * + * The esm module (required by dependencies) has compatibility issues with + * Node.js versions 22.10.0 to 22.17.x. We previously patched esm to work + * around this, but to avoid maintaining patches, we now check the version + * and provide clear guidance. + */ +function checkNodeVersion() { + const nodeVersion = process.version; + const parsedVersion = semver.parse(nodeVersion); + + /* eslint-disable no-console */ + + if (!parsedVersion) { + console.warn(chalk.yellow(`Warning: Could not parse Node.js version: ${nodeVersion}`)); + return; + } + + // Check if version is in the problematic range: >=22.10.0 <22.18.0 + const isProblematicVersion = semver.satisfies(nodeVersion, '>=22.10.0 <22.18.0'); + + if (isProblematicVersion) { + console.error('\n'); + console.error(chalk.red('='.repeat(80))); + console.error(chalk.red('ERROR: Incompatible Node.js version detected')); + console.error(chalk.red('='.repeat(80))); + console.error(); + console.error(chalk.yellow(`Current Node.js version: ${chalk.bold(nodeVersion)}`)); + console.error(); + console.error(chalk.white('The Ember admin build requires the esm module, which has compatibility')); + console.error(chalk.white('issues with Node.js versions 22.10.0 through 22.17.x.')); + console.error(); + console.error(chalk.white('Please use one of the following Node.js versions:')); + console.error(chalk.green(' • Node.js 22.18.0 or later')); + console.error(); + console.error(chalk.white('To switch Node.js versions, you can use a version manager:')); + console.error(chalk.cyan(' • nvm: nvm install 22.18.0 && nvm use 22.18.0')); + console.error(); + console.error(chalk.red('='.repeat(80))); + console.error(); + + process.exit(1); + } +} + +module.exports = checkNodeVersion; diff --git a/ghost/admin/mirage/.eslintrc.js b/ghost/admin/mirage/.eslintrc.js new file mode 100644 index 0000000..9c02c84 --- /dev/null +++ b/ghost/admin/mirage/.eslintrc.js @@ -0,0 +1,6 @@ +/* eslint-env node */ +module.exports = { + rules: { + 'brace-style': 'off' + } +}; diff --git a/ghost/admin/mirage/config.js b/ghost/admin/mirage/config.js new file mode 100644 index 0000000..a6a68a2 --- /dev/null +++ b/ghost/admin/mirage/config.js @@ -0,0 +1,26 @@ +/* eslint-disable ghost/ember/no-test-import-export */ +import {applyEmberDataSerializers, discoverEmberDataModels} from 'ember-cli-mirage'; +import {createServer} from 'miragejs'; +import {isTesting, macroCondition} from '@embroider/macros'; + +import devRoutes from './routes-dev'; +import testRoutes from './routes-test'; + +export default function (config) { + let finalConfig = { + ...config, + models: {...discoverEmberDataModels(), ...config.models}, + serializers: applyEmberDataSerializers(config.serializers), + routes + }; + + return createServer(finalConfig); +} + +function routes() { + if (macroCondition(isTesting())) { + testRoutes.call(this); + } else { + devRoutes.call(this); + } +} diff --git a/ghost/admin/mirage/routes-dev.js b/ghost/admin/mirage/routes-dev.js new file mode 100644 index 0000000..2f9fe15 --- /dev/null +++ b/ghost/admin/mirage/routes-dev.js @@ -0,0 +1,25 @@ +import ghostPaths from 'ghost-admin/utils/ghost-paths'; + +export default function () { + // allow any local requests outside of the namespace (configured below) to hit the real server + // _must_ be called before the namespace property is set + this.passthrough('/ghost/assets/**'); + + this.namespace = ghostPaths().apiRoot; + this.timing = 1000; // delay for each request, automatically set to 0 during testing + this.logging = true; + + // Mock endpoints here to override real API requests during development, eg... + // this.put('/posts/:id/', versionMismatchResponse); + // mockTags(this); + // this.loadFixtures('settings'); + + // keep this line, it allows all other API requests to hit the real server + this.passthrough(); + + // add any external domains to make sure those get passed through too + this.passthrough('http://www.gravatar.com/**'); + this.passthrough('https://cdn.jsdelivr.net/**'); + this.passthrough('https://api.unsplash.com/**'); + this.passthrough('https://ghost.org/**'); +} diff --git a/ghost/admin/mirage/routes-test.js b/ghost/admin/mirage/routes-test.js new file mode 100644 index 0000000..0f812bf --- /dev/null +++ b/ghost/admin/mirage/routes-test.js @@ -0,0 +1,143 @@ +import ghostPaths from 'ghost-admin/utils/ghost-paths'; + +import mockApiKeys from './config/api-keys'; +import mockAuthentication from './config/authentication'; +import mockConfig from './config/config'; +import mockEmailPreview from './config/email-preview'; +import mockEmails from './config/emails'; +import mockIntegrations from './config/integrations'; +import mockInvites from './config/invites'; +import mockLabels from './config/labels'; +import mockMembers from './config/members'; +import mockNewsletters from './config/newsletters'; +import mockOffers from './config/offers'; +import mockPages from './config/pages'; +import mockPosts from './config/posts'; +import mockRoles from './config/roles'; +import mockSearchIndex from './config/search-index'; +import mockSettings from './config/settings'; +import mockSite from './config/site'; +import mockSlugs from './config/slugs'; +import mockSnippets from './config/snippets'; +import mockStats from './config/stats'; +import mockTags from './config/tags'; +import mockThemes from './config/themes'; +import mockTiers from './config/tiers'; +import mockUploads from './config/uploads'; +import mockUsers from './config/users'; +import mockWebhooks from './config/webhooks'; + +/* eslint-disable ghost/ember/no-test-import-export */ +export default function () { + this.namespace = ghostPaths().apiRoot; + // this.timing = 400; // delay for each request, automatically set to 0 during testing + this.logging = false; + + mockApiKeys(this); + mockAuthentication(this); + mockConfig(this); + mockEmailPreview(this); + mockEmails(this); + mockIntegrations(this); + mockInvites(this); + mockMembers(this); + mockLabels(this); + mockPages(this); + mockPosts(this); + mockRoles(this); + mockSearchIndex(this); + mockSettings(this); + mockSite(this); + mockSlugs(this); + mockTags(this); + mockThemes(this); + mockUploads(this); + mockUsers(this); + mockWebhooks(this); + mockTiers(this); + mockOffers(this); + mockSnippets(this); + mockNewsletters(this); + mockStats(this); + + /* Notifications -------------------------------------------------------- */ + + this.get('/notifications/'); + + /* Integrations - Slack Test Notification ------------------------------- */ + + this.post('/slack/test', function () { + return {}; + }); + + /* Delete all ----------------------------------------------------------- */ + + this.del('/db', function (db) { + db.posts.all().remove(); + db.tags.all().remove(); + }, 204); + + /* limit=all blocker ---------------------------------------------------- */ + const originalHandledRequest = this.pretender.handledRequest; + this.pretender.handledRequest = function (verb, path, request) { + originalHandledRequest.call(this, verb, path, request); + + const url = new URL(request.url, window.location.origin); + const limit = url.searchParams.get('limit'); + + const ALLOWED_LIMIT_ALL = [ + '/api/admin/members/upload/' + ]; + + // limit=all is completely blocked, we shouldn't have any requests reach the server with this + if (limit === 'all' && !ALLOWED_LIMIT_ALL.some(allowed => path.includes(allowed))) { + throw new Error(`Blocked mirage request with limit=all: ${verb} ${path}.`); + } + + // limit > 100 is also blocked, apart from some specific endpoints + if (limit && parseInt(limit, 10) > 100) { + const parsedLimit = parseInt(limit, 10); + + const SEARCH_ENDPOINTS_REGEX = /\/api\/admin\/(posts|pages|tags|users)\//; + if (parsedLimit === 10000 && SEARCH_ENDPOINTS_REGEX.test(path)) { + return; + } + + if (path.match(/\/emails\/.*\/batches\//)) { + return; + } + + if (path.includes('/api/admin/posts/export/')) { + return; + } + + throw new Error(`Blocked mirage request with limit > 100: ${verb} ${path}.`); + } + }; + + /* External sites ------------------------------------------------------- */ + + this.head('http://www.gravatar.com/avatar/:md5', function () { + return ''; + }, 200); + + this.get('http://www.gravatar.com/avatar/:md5', function () { + return ''; + }, 200); + + this.get('https://ghost.org/changelog.json', function () { + return { + posts: [ + { + title: 'Custom image alt tags', + custom_excerpt: 'Alt tag support for images in the Ghost editor', + slug: 'image-alt-text-support', + published_at: '2019-08-05T07:46:16.000+00:00', + url: 'https://ghost.org/changelog/image-alt-text-support/', + featured: 'false' + } + ], + changelogUrl: 'https://ghost.org/changelog/' + }; + }); +} diff --git a/ghost/admin/mirage/utils.js b/ghost/admin/mirage/utils.js new file mode 100644 index 0000000..3a979b2 --- /dev/null +++ b/ghost/admin/mirage/utils.js @@ -0,0 +1,160 @@ +import {Response} from 'miragejs'; +import {isArray} from '@ember/array'; + +export function paginatedResponse(modelName) { + return function (schema, request) { + let page = +request.queryParams.page || 1; + let limit = request.queryParams.limit; + let collection = schema[modelName].all(); + + if (limit !== 'all') { + limit = +request.queryParams.limit || 15; + } + + return paginateModelCollection(modelName, collection, page, limit); + }; +} + +export function paginateModelCollection(modelName, collection, page, limit) { + let pages, next, prev, models; + + page = parseInt(page, 10); + limit = parseInt(limit, 10); + + if (limit === 'all') { + pages = 1; + } else { + limit = +limit; + + let start = (page - 1) * limit; + let end = start + limit; + + pages = Math.ceil(collection.models.length / limit); + models = collection.models.slice(start, end); + + if (start > 0) { + prev = page - 1; + } + + if (end < collection.models.length) { + next = page + 1; + } + } + + collection.meta = { + pagination: { + page, + limit, + pages, + total: collection.models.length, + next: next || null, + prev: prev || null + } + }; + + if (models) { + collection.models = models; + } + + return collection; +} + +export function maintenanceResponse() { + return new Response(503, {}, { + errors: [{ + type: 'Maintenance' + }] + }); +} + +export function versionMismatchResponse() { + return new Response(400, {}, { + errors: [{ + type: 'VersionMismatchError' + }] + }); +} + +function normalizeBooleanParams(arr) { + if (!isArray(arr)) { + return arr; + } + + return arr.map((i) => { + if (i === 'true') { + return true; + } else if (i === 'false') { + return false; + } else { + return i; + } + }); +} + +function normalizeStringParams(arr) { + if (!isArray(arr)) { + return arr; + } + + return arr.map((i) => { + if (!i.replace) { + return i; + } + + return i.replace(/^['"]|['"]$/g, ''); + }); +} + +// TODO: use GQL to parse filter string? +export function extractFilterParam(param, filter = '') { + let filterRegex = new RegExp(`${param}:(.*?)(?:\\+|$)`); + let match; + + let [, result] = filter.match(filterRegex) || []; + + if (!result) { + return; + } + + if (result.startsWith('[')) { + match = result.replace(/^\[|\]$/g, '').split(','); + } else if (result.startsWith('~')) { + match = result.replace(/^~/, '').replace(/\\'/g, `'`).replace(/^'|'$/g, ''); + } else { + match = [result]; + } + + return normalizeBooleanParams(normalizeStringParams(match)); +} + +export function hasInvalidPermissions(allowedRoles) { + const {schema, request} = this; + + // always allow dev requests through - the logged in user will be real so + // we can't check against it in the mocked db + if (!request.requestHeaders['X-Test-User']) { + return false; + } + + const invalidPermsResponse = new Response(403, {}, { + errors: [{ + type: 'NoPermissionError', + message: 'You do not have permission to perform this action' + }] + }); + + const user = schema.users.find(request.requestHeaders['X-Test-User']); + const adminRoles = user.roles.filter(role => allowedRoles.includes(role.name)); + + if (adminRoles.length === 0) { + return invalidPermsResponse; + } +} + +export function withPermissionsCheck(allowedRoles, fn) { + return function () { + const boundPermsCheck = hasInvalidPermissions.bind(this); + const boundFn = fn.bind(this); + return boundPermsCheck(allowedRoles) || boundFn(...arguments); + }; +} diff --git a/ghost/admin/package.json b/ghost/admin/package.json new file mode 100644 index 0000000..f12aef8 --- /dev/null +++ b/ghost/admin/package.json @@ -0,0 +1,230 @@ +{ + "name": "ghost-admin", + "version": "6.33.0-rc.0", + "description": "Ember.js admin client for Ghost", + "author": "Ghost Foundation", + "homepage": "http://ghost.org", + "repository": { + "type": "git", + "url": "git://github.com/TryGhost/Admin.git" + }, + "bugs": "https://github.com/TryGhost/Ghost/issues", + "contributors": "https://github.com/TryGhost/Admin/graphs/contributors", + "license": "MIT", + "private": true, + "directories": { + "test": "tests" + }, + "scripts": { + "dev": "ember serve", + "build": "ember build --environment=production --silent", + "build:dev": "pnpm build --environment=development", + "test:unit": "true", + "test": "ember exam --split 2 --parallel", + "lint:js": "eslint . --cache", + "lint:hbs": "ember-template-lint .", + "lint": "pnpm lint:js && pnpm lint:hbs" + }, + "engines": { + "node": "^22.13.1" + }, + "devDependencies": { + "@babel/eslint-parser": "7.28.4", + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/plugin-proposal-decorators": "7.28.0", + "@ember/jquery": "2.0.0", + "@ember/optional-features": "2.1.0", + "@ember/render-modifiers": "2.1.0", + "@ember/test-helpers": "2.9.6", + "@ember/test-waiters": "3.1.0", + "@embroider/macros": "1.16.13", + "@faker-js/faker": "7.6.0", + "@glimmer/component": "1.1.2", + "@html-next/vertical-collection": "3.0.0", + "@sentry/ember": "7.120.3", + "@sentry/integrations": "7.114.0", + "@sentry/replay": "7.116.0", + "@tryghost/admin-x-framework": "workspace:*", + "ghost": "workspace:*", + "@tryghost/color-utils": "0.2.16", + "@tryghost/ember-promise-modals": "2.0.1", + "@tryghost/helpers": "1.1.103", + "@tryghost/kg-clean-basic-html": "4.2.23", + "@tryghost/kg-converters": "1.1.21", + "@tryghost/koenig-lexical": "1.7.30", + "@tryghost/limit-service": "1.5.2", + "@tryghost/members-csv": "2.0.5", + "@tryghost/nql": "0.12.10", + "@tryghost/nql-lang": "0.6.4", + "@tryghost/string": "0.3.2", + "@tryghost/timezone-data": "0.4.18", + "animejs": "3.2.2", + "autoprefixer": "9.8.6", + "babel-plugin-transform-class-properties": "6.24.1", + "babel-plugin-transform-react-jsx": "6.24.1", + "broccoli-asset-rev": "3.0.0", + "broccoli-concat": "4.2.7", + "broccoli-funnel": "3.0.8", + "broccoli-merge-trees": "4.2.0", + "broccoli-terser-sourcemap": "4.1.1", + "chai": "4.5.0", + "chai-dom": "1.12.1", + "codemirror": "5.48.2", + "cssnano": "4.1.10", + "element-resize-detector": "1.2.4", + "ember-ajax": "5.1.2", + "ember-assign-helper": "0.5.0", + "ember-auto-import": "2.10.0", + "ember-classic-decorator": "3.0.1", + "ember-cli": "3.24.0", + "ember-cli-app-version": "5.0.0", + "ember-cli-babel": "8.2.0", + "ember-cli-chart": "3.7.2", + "ember-cli-code-coverage": "1.0.3", + "ember-cli-dependency-checker": "3.3.2", + "ember-cli-deprecation-workflow": "2.2.0", + "ember-cli-htmlbars": "6.3.0", + "ember-cli-inject-live-reload": "2.1.0", + "ember-cli-mirage": "2.4.0", + "ember-cli-node-assets": "0.2.2", + "ember-cli-postcss": "6.0.1", + "ember-cli-shims": "1.2.0", + "ember-cli-string-helpers": "6.1.0", + "ember-cli-terser": "4.0.1", + "ember-cli-test-loader": "3.1.0", + "ember-composable-helpers": "5.0.0", + "ember-concurrency": "2.3.7", + "ember-could-get-used-to-this": "1.0.1", + "ember-css-transitions": "4.4.1", + "ember-data": "3.24.0", + "ember-decorators": "6.1.1", + "ember-drag-drop": "0.4.8", + "ember-ella-sparse": "0.16.0", + "ember-exam": "6.0.1", + "ember-export-application-global": "2.0.1", + "ember-fetch": "8.1.2", + "ember-in-viewport": "4.1.0", + "ember-infinity": "2.3.0", + "ember-keyboard": "8.2.1", + "ember-load": "0.0.17", + "ember-load-initializers": "2.1.2", + "ember-mocha": "0.16.2", + "ember-modifier": "4.2.0", + "ember-moment": "10.0.1", + "ember-one-way-select": "4.0.1", + "ember-power-datepicker": "0.8.1", + "ember-power-select": "6.0.1", + "ember-resolver": "8.1.0", + "ember-simple-auth": "5.0.0", + "ember-sinon": "5.0.0", + "ember-source": "3.24.0", + "ember-svg-jar": "2.7.1", + "ember-template-lint": "5.13.0", + "ember-test-selectors": "6.0.0", + "ember-tooltips": "3.6.0", + "ember-truth-helpers": "3.1.1", + "eslint": "catalog:", + "eslint-plugin-babel": "5.3.1", + "flexsearch": "0.7.43", + "fs-extra": "11.3.4", + "glob": "8.1.0", + "google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost", + "keymaster": "https://github.com/madrobby/keymaster.git", + "liquid-fire": "0.34.0", + "liquid-wormhole": "3.0.1", + "loader.js": "4.7.0", + "microdiff": "1.5.0", + "miragejs": "0.1.48", + "moment-timezone": "0.5.45", + "normalize.css": "3.0.3", + "papaparse": "5.5.3", + "postcss-color-mod-function": "3.0.3", + "postcss-custom-media": "7.0.8", + "postcss-custom-properties": "10.0.0", + "postcss-import": "12.0.1", + "pretender": "3.4.7", + "process": "0.11.10", + "react": "18.3.1", + "react-dom": "18.3.1", + "reframe.js": "4.0.2", + "semver": "7.7.4", + "sentry-testkit": "5.0.10", + "sinon-chai": "4.0.1", + "testem": "3.19.1", + "tracked-built-ins": "3.4.0", + "util": "0.12.5", + "validator": "13.12.0", + "walk-sync": "3.0.0" + }, + "ember-addon": { + "paths": [ + "lib/asset-delivery", + "lib/ember-power-calendar-moment", + "lib/ember-power-calendar-utils" + ] + }, + "ember": { + "edition": "octane" + }, + "lint-staged": { + "*.hbs": "ember-template-lint", + "*.js": "eslint" + }, + "dependencies": { + "i18n-iso-countries": "7.14.0", + "lru-cache": "6.0.0", + "path-browserify": "1.0.1", + "webpack": "5.105.4" + }, + "nx": { + "implicitDependencies": [ + "!ghost" + ], + "targets": { + "build:dev": { + "dependsOn": [ + "build:dev", + { + "projects": [ + "@tryghost/admin-x-framework", + "@tryghost/admin-x-settings", + "@tryghost/activitypub", + "@tryghost/posts", + "@tryghost/stats" + ], + "target": "build" + } + ] + }, + "build": { + "outputs": [ + "{projectRoot}/dist", + "{workspaceRoot}/ghost/core/core/built/admin" + ], + "dependsOn": [ + "build", + { + "projects": [ + "@tryghost/admin-x-framework", + "@tryghost/admin-x-settings", + "@tryghost/activitypub", + "@tryghost/posts", + "@tryghost/stats" + ], + "target": "build" + } + ] + }, + "test": { + "dependsOn": [ + { + "projects": [ + "@tryghost/admin-x-framework" + ], + "target": "build" + } + ] + } + } + } +} diff --git a/ghost/admin/testem.js b/ghost/admin/testem.js new file mode 100644 index 0000000..3f482cb --- /dev/null +++ b/ghost/admin/testem.js @@ -0,0 +1,36 @@ +/* eslint-env node */ +/* eslint-disable camelcase */ + +let launch_in_ci = [process.env.BROWSER || 'Chrome']; + +module.exports = { + framework: 'mocha', + browser_start_timeout: 120, + browser_disconnect_timeout: 60, + test_page: 'tests/index.html?hidepassed', + disable_watching: true, + parallel: process.env.EMBER_EXAM_SPLIT_COUNT || 1, + launch_in_ci, + launch_in_dev: [ + 'Chrome', + 'Firefox' + ], + browser_args: { + Chrome: { + ci: [ + // --no-sandbox is needed when running Chrome inside a container + process.env.CI ? '--no-sandbox' : null, + '--headless', + '--disable-dev-shm-usage', + '--disable-software-rasterizer', + '--mute-audio', + '--remote-debugging-port=0', + '--window-size=1440,900' + ].filter(Boolean) + }, + Firefox: { + ci: ['-headless'] + } + }, + tap_failed_tests_only: true +}; diff --git a/ghost/admin/tests/index.html b/ghost/admin/tests/index.html new file mode 100644 index 0000000..661d421 --- /dev/null +++ b/ghost/admin/tests/index.html @@ -0,0 +1,51 @@ + + + + + + + Ghost Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/ghost/admin/tests/test-helper.js b/ghost/admin/tests/test-helper.js new file mode 100644 index 0000000..9152f80 --- /dev/null +++ b/ghost/admin/tests/test-helper.js @@ -0,0 +1,22 @@ +import Application from 'ghost-admin/app'; +import config from 'ghost-admin/config/environment'; +import registerWaiter from 'ember-raf-scheduler/test-support/register-waiter'; +import start from 'ember-exam/test-support/start'; +import {setApplication} from '@ember/test-helpers'; + +import chai from 'chai'; +import chaiDom from 'chai-dom'; +import sinonChai from 'sinon-chai'; +chai.use(chaiDom); +chai.use(sinonChai); + +setApplication(Application.create(config.APP)); + +registerWaiter(); + +mocha.setup({ + timeout: 15000, + slow: 500 +}); + +start(); diff --git a/ghost/core/.c8rc.e2e.json b/ghost/core/.c8rc.e2e.json new file mode 100644 index 0000000..98ec501 --- /dev/null +++ b/ghost/core/.c8rc.e2e.json @@ -0,0 +1,27 @@ +{ + "all": true, + "check-coverage": true, + "reporter": [ + "html-spa", + "text-summary", + "cobertura" + ], + "reportsDir": "./coverage-e2e", + "statements": 54, + "branches": 75, + "functions": 79, + "lines": 54, + "include": [ + "core/{*.js,frontend,server,shared}" + ], + "exclude": [ + "core/frontend/src/**", + "core/frontend/public/**", + "core/frontend/helpers/**", + "core/server/data/migrations/**", + "core/server/data/schema/schema.js", + "!core/server/data/migrations/utils.js", + "core/server/web/api/testmode/**", + "core/server/services/koenig/**" + ] +} diff --git a/ghost/core/.c8rc.json b/ghost/core/.c8rc.json new file mode 100644 index 0000000..9917a25 --- /dev/null +++ b/ghost/core/.c8rc.json @@ -0,0 +1,35 @@ +{ + "all": true, + "check-coverage": true, + "reporter": [ + "html-spa", + "text-summary", + "cobertura" + ], + "statements": 65, + "branches": 85, + "functions": 65, + "lines": 65, + "include": [ + "core/{*.js,frontend,server,shared}" + ], + "exclude": [ + "core/frontend/src/**", + "core/frontend/public/**", + "core/server/data/migrations/**", + "core/server/data/schema/schema.js", + "!core/server/data/migrations/utils.js", + "core/frontend/web/**", + "!core/frontend/web/middleware/**", + "core/server/web/**/app.js", + "core/server/web/api/testmode/**", + "core/server/web/parent/**", + "core/server/api/endpoints/**", + "!core/server/web/**/middleware/**", + "!core/server/api/endpoints/utils", + "core/server/services/email-analytics/jobs/**", + "core/server/services/members/jobs/**", + "core/server/services/email-service/wrapper.js", + "core/server/services/**/service.js" + ] +} diff --git a/ghost/core/.eslintignore b/ghost/core/.eslintignore new file mode 100644 index 0000000..9adadb0 --- /dev/null +++ b/ghost/core/.eslintignore @@ -0,0 +1,4 @@ +core/frontend/src/**/*.js +!core/frontend/src/member-attribution/*.js +core/frontend/public/**/*.js +core/built diff --git a/ghost/core/.eslintrc.js b/ghost/core/.eslintrc.js new file mode 100644 index 0000000..a07a00f --- /dev/null +++ b/ghost/core/.eslintrc.js @@ -0,0 +1,160 @@ +const path = require('path'); + +module.exports = { + env: { + es6: true, + node: true + }, + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ], + rules: { + 'no-var': 'error', + 'one-var': ['error', 'never'] + }, + overrides: [ + { + files: [ + '**/*.ts' + ], + extends: [ + 'plugin:ghost/ts' + ], + parser: '@typescript-eslint/parser' + }, + { + files: 'core/server/api/endpoints/*', + rules: { + 'ghost/ghost-custom/max-api-complexity': 'error' + } + }, + { + files: 'core/server/data/migrations/versions/**', + excludedFiles: [ + 'core/server/data/migrations/versions/1.*/*', + 'core/server/data/migrations/versions/2.*/*', + 'core/server/data/migrations/versions/3.*/*' + ], + rules: { + 'ghost/filenames/match-regex': ['error', '^(?:\\d{4}(?:-\\d{2}){4,5}|\\d{2})(?:-[a-zA-Z0-9]+){2,}$', true] + } + }, + { + files: 'core/server/data/migrations/versions/**', + rules: { + 'no-restricted-syntax': ['error', { + selector: 'ForStatement', + message: 'For statements can perform badly in migrations' + }, { + selector: 'ForOfStatement', + message: 'For statements can perform badly in migrations' + }, { + selector: 'ForInStatement', + message: 'For statements can perform badly in migrations' + }, { + selector: 'WhileStatement', + message: 'While statements can perform badly in migrations' + }, { + selector: 'CallExpression[callee.property.name=\'forEach\']', + message: 'Loop constructs like forEach can perform badly in migrations' + }, { + selector: 'CallExpression[callee.object.name=\'_\'][callee.property.name=\'each\']', + message: 'Loop constructs like _.each can perform badly in migrations' + }, { + selector: 'CallExpression[callee.property.name=/join|innerJoin|leftJoin/] CallExpression[callee.property.name=/join|innerJoin|leftJoin/] CallExpression[callee.name=\'knex\']', + message: 'Use of multiple join statements in a single knex block' + }], + 'ghost/no-return-in-loop/no-return-in-loop': ['error'] + } + }, + { + files: 'core/shared/**', + rules: { + 'ghost/node/no-restricted-require': ['error', [ + { + name: path.resolve(__dirname, 'core/server/**'), + message: 'Invalid require of core/server from core/shared.' + }, + { + name: path.resolve(__dirname, 'core/frontend/**'), + message: 'Invalid require of core/frontend from core/shared.' + } + ]] + } + }, + { + files: 'core/server/data/schema/schema.js', + rules: { + 'no-restricted-syntax': ['error', + { + selector: 'Property[key.name="created_by"]', + message: '`created_by` is not allowed - The action log should be used to record user actions.' + }, + { + selector: 'Property[key.name="updated_by"]', + message: '`updated_by` is not allowed - The action log should be used to record user actions.' + } + ] + } + }, + { + // Enforce kebab-case filenames across core/ + // Excludes folders for special cases like adapters which need specific file namings + files: [ + 'core/**/*.{js,ts}' + ], + excludedFiles: [ + // Adapter filenames must match the name specified in config (e.g. adapters.cache.active: "Redis"). + // The adapter-manager loads adapters by constructing a path from the config value. + // See: core/shared/config/defaults.json, core/server/services/adapter-manager + 'core/server/adapters/**' + ], + rules: { + 'ghost/filenames/match-exported-class': 'off', + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } + }, + { + // Helper filenames use underscores because they map directly to Handlebars helper names + // e.g., ghost_head.js → {{ghost_head}}. Renaming would break all themes. + // See: core/frontend/services/helpers/registry.js:26 + files: ['core/frontend/helpers/**', 'core/frontend/apps/*/lib/helpers/**'], + rules: { + 'ghost/filenames/match-regex': ['error', '^[a-z0-9_.-]+$', false] + } + }, + /** + * @TODO: enable these soon + */ + { + files: 'core/frontend/**', + rules: { + 'ghost/node/no-restricted-require': ['off', [ + // If we make the frontend entirely independent, these have to be solved too + // { + // name: path.resolve(__dirname, 'core/shared/**'), + // message: 'Invalid require of core/shared from core/frontend.' + // }, + // These are critical refactoring issues that we need to tackle ASAP + { + name: [path.resolve(__dirname, 'core/server/**')], + message: 'Invalid require of core/server from core/frontend.' + } + ]] + } + }, + { + files: 'core/server/**', + rules: { + 'ghost/node/no-restricted-require': ['warn', [ + { + // Throw an error for all requires of the frontend, _except_ the url service which will be moved soon + name: [path.resolve(__dirname, 'core/frontend/**')], + message: 'Invalid require of core/frontend from core/server.' + } + ]] + } + } + ] +}; diff --git a/ghost/core/.npmignore b/ghost/core/.npmignore new file mode 100644 index 0000000..166d513 --- /dev/null +++ b/ghost/core/.npmignore @@ -0,0 +1,72 @@ +!** +.build +.dist +.tmp +Gruntfile.js +ghost-*.tgz +.eslintignore +.eslintrc.json +.eslintrc.js +.npmignore +.c8rc*.json +playwright.config.js +jsconfig.json +changelog.md* +coverage/** +docs/** +_site/** +content/adapters/** +!content/adapters/README.md +content/apps/** +!content/apps/README.md +content/public/** +!content/public/README.md +content/data/** +!content/data/README.md +content/images/** +!content/images/README.md +content/logs/** +!content/logs/README.md +content/settings/** +!content/settings/README.md +content/themes/** +!content/themes/casper/** +content/themes/casper/pnpm-lock.yaml +!content/themes/source/** +content/themes/source/pnpm-lock.yaml +node_modules/** +core/server/lib/members/static/auth/node_modules/** +**/*.db +**/*.db-journal +*.db +*.db-journal +.af* +.git* +.groc* +.jshintrc +.jscsrc +*.iml +core/built/**/*.map +core/built/**/test-* +core/built/**/tests-* +test/** +CONTRIBUTING.md +content/themes/casper/SECURITY.md +content/themes/source/SECURITY.md +SECURITY.md +renovate.json +*.html +!core/frontend/src/admin-auth/*.html +!core/built/admin/**/*.html +!core/server/views/** +!core/server/services/mail/templates/** +bower_components/** +.editorconfig +monobundle.js +!content/themes/casper/gulpfile.js +!content/themes/source/gulpfile.js +package-lock.json +content/themes/casper/config.*.json +content/themes/source/config.*.json +config.*.json +!core/shared/config/env/** diff --git a/ghost/core/MigratorConfig.js b/ghost/core/MigratorConfig.js new file mode 100644 index 0000000..1ebd030 --- /dev/null +++ b/ghost/core/MigratorConfig.js @@ -0,0 +1,34 @@ +/** + * knex-migrator requires this exact filename in the project root, therefore, linter naming rules are disabled here. + * @see https://github.com/TryGhost/knex-migrator + */ +/* eslint-disable ghost/filenames/match-regex */ +const config = require('./core/shared/config'); +const ghostVersion = require('@tryghost/version'); + +/** + * knex-migrator can be used via CLI or within the application + * when using the CLI, we need to ensure that our global overrides are triggered + */ +require('./core/server/overrides'); + +/** + * Register tsx so that require() can resolve .ts files used in server code. + * + * tsx is a devDependency, so this is a no-op in production where Ghost runs + * migrations on boot through its own process (which uses --import=tsx) or in + * CI environments where a build has already run and the TS is already compiled + */ +try { + require('tsx/cjs'); +} catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } +} + +module.exports = { + currentVersion: ghostVersion.safe, + database: config.get('database'), + migrationPath: config.get('paths:migrationPath') +}; diff --git a/ghost/core/bin/create-migration.js b/ghost/core/bin/create-migration.js new file mode 100644 index 0000000..3cb5b1d --- /dev/null +++ b/ghost/core/bin/create-migration.js @@ -0,0 +1,128 @@ +#!/usr/bin/env node +/* eslint-disable no-console, ghost/ghost-custom/no-native-error */ + +const path = require('path'); +const fs = require('fs'); +const semver = require('semver'); + +const MIGRATION_TEMPLATE = `const logging = require('@tryghost/logging'); + +// For DDL - schema changes +// const {createNonTransactionalMigration} = require('../../utils'); + +// For DML - data changes +// const {createTransactionalMigration} = require('../../utils'); + +// Or use a specific helper +// const {addTable, createAddColumnMigration} = require('../../utils'); + +module.exports = /**/; +`; + +const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; + +/** + * Validates that a slug is kebab-case (lowercase alphanumeric with single hyphens). + */ +function isValidSlug(slug) { + return typeof slug === 'string' && SLUG_PATTERN.test(slug); +} + +/** + * Returns the migration version folder name for the given package version. + * + * semver.inc(v, 'minor') handles both cases: + * - Stable 6.18.0 → 6.19.0 (increments minor) + * - Prerelease 6.19.0-rc.0 → 6.19.0 (strips prerelease, keeps minor) + * + * Key invariant: 6.18.0 and 6.19.0-rc.0 both produce folder "6.19". + */ +function getNextMigrationVersion(version) { + const next = semver.inc(version, 'minor'); + if (!next) { + throw new Error(`Invalid version: ${version}`); + } + return `${semver.major(next)}.${semver.minor(next)}`; +} + +/** + * Creates a migration file and optionally bumps package versions to RC. + * + * @param {object} options + * @param {string} options.slug - The migration name in kebab-case + * @param {string} options.coreDir - Path to ghost/core directory + * @param {Date} [options.date] - Override the timestamp (for testing) + * @returns {{migrationPath: string, rcVersion: string|null}} + */ +function createMigration({slug, coreDir, date}) { + if (!isValidSlug(slug)) { + throw new Error(`Invalid slug: "${slug}". Use kebab-case (e.g. add-column-to-posts)`); + } + + const migrationsDir = path.join(coreDir, 'core', 'server', 'data', 'migrations', 'versions'); + const corePackagePath = path.join(coreDir, 'package.json'); + + const corePackage = JSON.parse(fs.readFileSync(corePackagePath, 'utf8')); + const currentVersion = corePackage.version; + + const nextVersion = getNextMigrationVersion(currentVersion); + const versionDir = path.join(migrationsDir, nextVersion); + + const timestamp = (date || new Date()).toISOString().slice(0, 19).replace('T', '-').replaceAll(':', '-'); + const filename = `${timestamp}-${slug}.js`; + const migrationPath = path.join(versionDir, filename); + + fs.mkdirSync(versionDir, {recursive: true}); + try { + fs.writeFileSync(migrationPath, MIGRATION_TEMPLATE, {flag: 'wx'}); + } catch (err) { + if (err.code === 'EEXIST') { + throw new Error(`Migration already exists: ${migrationPath}`); + } + throw err; + } + + // Auto-bump to RC if this is a stable version + let rcVersion = null; + if (!semver.prerelease(currentVersion)) { + rcVersion = semver.inc(currentVersion, 'preminor', 'rc'); + + corePackage.version = rcVersion; + fs.writeFileSync(corePackagePath, JSON.stringify(corePackage, null, 2) + '\n'); + + const adminPackagePath = path.resolve(coreDir, '..', 'admin', 'package.json'); + if (fs.existsSync(adminPackagePath)) { + const adminPackage = JSON.parse(fs.readFileSync(adminPackagePath, 'utf8')); + adminPackage.version = rcVersion; + fs.writeFileSync(adminPackagePath, JSON.stringify(adminPackage, null, 2) + '\n'); + } + } + + return {migrationPath, rcVersion}; +} + +// CLI entry point +if (require.main === module) { + const slug = process.argv[2]; + + if (!slug) { + console.error('Usage: pnpm migrate:create '); + console.error(' slug: kebab-case migration name (e.g. add-column-to-posts)'); + process.exit(1); + } + + try { + const coreDir = path.resolve(__dirname, '..'); + const {migrationPath, rcVersion} = createMigration({slug, coreDir}); + + console.log(`Created migration: ${migrationPath}`); + if (rcVersion) { + console.log(`Bumped version to ${rcVersion}`); + } + } catch (err) { + console.error(err.message); + process.exit(1); + } +} + +module.exports = {isValidSlug, getNextMigrationVersion, createMigration}; diff --git a/ghost/core/bin/generate-golden-email.js b/ghost/core/bin/generate-golden-email.js new file mode 100644 index 0000000..7e89743 --- /dev/null +++ b/ghost/core/bin/generate-golden-email.js @@ -0,0 +1,132 @@ +#!/usr/bin/env node +// @ts-check + +/* eslint-disable no-console */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const jwt = require('jsonwebtoken'); + +const GHOST_URL = process.env.GHOST_URL || 'http://localhost:2368'; +const GOLDEN_POST_PATH = path.join(__dirname, '..', 'test', 'utils', 'fixtures', 'email-service', 'golden-post.json'); + +function usage() { + console.error('Usage: GHOST_GOLDEN_POST_AUTH=id:secret pnpm generate-golden-email '); + console.error(''); + console.error(' segment: Member segment filter, e.g. "status:free" or "status:-free"'); + console.error(' output-path: Path to write the rendered email HTML'); + console.error(''); + console.error('GHOST_GOLDEN_POST_AUTH should be an Admin API key in id:secret format.'); + console.error('Requires a running Ghost dev instance (pnpm dev).'); + process.exit(1); +} + +/** + * @returns {{id: string, secret: string}} + */ +function getAdminApiKey() { + const auth = process.env.GHOST_GOLDEN_POST_AUTH; + if (!auth) { + console.error('Error: GHOST_GOLDEN_POST_AUTH environment variable is required.'); + console.error('Set it to an Admin API key.'); + process.exit(1); + } + const [id, secret] = auth.split(':'); + if (id && secret) { + return {id, secret}; + } + console.error('Error: GHOST_GOLDEN_POST_AUTH environment variable is invalid.'); + process.exit(1); +} + +/** + * @param {{id: string, secret: string}} apiKey + * @returns {string} + */ +function signToken(apiKey) { + return jwt.sign( + {}, + Buffer.from(apiKey.secret, 'hex'), + { + keyid: apiKey.id, + algorithm: 'HS256', + expiresIn: '5m', + audience: '/admin/' + } + ); +} + +/** + * @param {string} token + * @param {string} method + * @param {string} endpoint + * @param {object} [body] + * @returns {Promise>} + */ +async function apiRequest(token, method, endpoint, body) { + // god_mode bypasses the integration token endpoint allowlist in development + const separator = endpoint.includes('?') ? '&' : '?'; + const url = `${GHOST_URL}/ghost/api/admin/${endpoint}${separator}god_mode=true`; + const res = await fetch(url, { + method, + headers: { + Authorization: `Ghost ${token}`, + 'Content-Type': 'application/json' + }, + ...(body ? {body: JSON.stringify(body)} : {}) + }); + assert(res.ok, `API ${method} ${endpoint} failed (${res.status})`); + if (res.status === 204) { + return {}; + } + return await res.json(); +} + +async function main() { + const args = process.argv.slice(2); + if (args.length !== 2) { + usage(); + } + const [segment, outputPath] = args; + + const apiKey = getAdminApiKey(); + const token = signToken(apiKey); + + const goldenPost = JSON.parse(fs.readFileSync(GOLDEN_POST_PATH, 'utf8')); + + const createRes = await apiRequest(token, 'POST', 'posts/', { + posts: [{ + title: 'Golden Email Preview (temp)', + status: 'draft', + lexical: JSON.stringify(goldenPost) + }] + }); + const postId = createRes.posts[0].id; + + try { + const previewRes = await apiRequest( + token, + 'GET', + `email_previews/posts/${postId}/?memberSegment=${encodeURIComponent(segment)}` + ); + const html = previewRes.email_previews[0].html; + + const resolvedPath = path.resolve(outputPath); + fs.writeFileSync(resolvedPath, html, 'utf8'); + console.log(`Golden email written to ${resolvedPath}`); + } finally { + try { + await apiRequest(token, 'DELETE', `posts/${postId}/`); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error('Warning: failed to clean up draft post:', message); + } + } +} + +main().catch((err) => { + const message = err instanceof Error ? err.message : String(err); + console.error('Error:', message); + process.exit(1); +}); diff --git a/ghost/core/bin/minify-assets.js b/ghost/core/bin/minify-assets.js new file mode 100644 index 0000000..03b8657 --- /dev/null +++ b/ghost/core/bin/minify-assets.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node + +/** + * Script to minify multiple JavaScript files using esbuild + * Supports per-file configuration for bundling and other options + * + * The tryghost/minifier package is not used because it is intended to be used at runtime, + * allowing for replacements to be made on the fly (e.g. for card-assets and theme activation). + * + * This script is intended to be run at build time, allowing for us to use the minified files + * in the production build and be able to utilize bundler benefits. + */ + +const esbuild = require('esbuild'); +const path = require('path'); +const fs = require('fs'); +const logging = require('@tryghost/logging'); + +// Determine the root directory by checking for common project files +function findProjectRoot() { + let currentDir = process.cwd(); + + // Check if we're already in ghost/core + if (currentDir.endsWith('ghost/core') || currentDir.endsWith('ghost\\core')) { + return currentDir; + } + + // Look for ghost/core directory + const ghostCorePath = path.join(currentDir, 'ghost', 'core'); + if (fs.existsSync(ghostCorePath)) { + return ghostCorePath; + } + + return currentDir; +} + +const projectRoot = findProjectRoot(); +logging.debug(`Resolving paths from: ${projectRoot}`); + +// Helper to resolve paths relative to project root +function resolvePath(filePath) { + return path.join(projectRoot, filePath); +} + +// Define files to minify with their specific configuration +const filesToMinify = [ + { + src: 'core/frontend/src/comment-counts/comment-counts.js', + dest: 'core/frontend/public/comment-counts.min.js', + options: { + bundle: false + } + }, + { + src: 'core/frontend/src/ghost-stats/ghost-stats.js', + dest: 'core/frontend/public/ghost-stats.min.js', + options: { + bundle: true, + format: 'iife', + target: ['es2020'] + } + }, + { + src: 'core/frontend/src/member-attribution/member-attribution.js', + dest: 'core/frontend/public/member-attribution.min.js', + options: { + bundle: true, + format: 'iife', + target: ['es2020'] + } + }, + { + src: 'core/frontend/src/admin-auth/message-handler.js', + dest: 'core/frontend/public/admin-auth/admin-auth.min.js', + options: { + bundle: false + } + }, + { + src: 'core/frontend/public/private.js', + dest: 'core/frontend/public/private.min.js', + options: { + bundle: false + } + } +]; + +// Process all files +(async () => { + logging.debug('Starting JS minification...'); + + for (const file of filesToMinify) { + try { + const srcPath = resolvePath(file.src); + const destPath = resolvePath(file.dest); + + // Ensure the destination directory exists + const destDir = path.dirname(destPath); + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, {recursive: true}); + } + + // Create build configuration by merging default options with file-specific options + const buildConfig = { + entryPoints: [srcPath], + outfile: destPath, + minify: true, + platform: 'browser', + // Apply file-specific options, with defaults + ...file.options + }; + + await esbuild.build(buildConfig); + + // Show bundling status in output + const bundleStatus = buildConfig.bundle ? 'bundled + minified' : 'minified'; + logging.debug(`✓ ${file.src} → ${file.dest} (${bundleStatus})`); + } catch (error) { + console.error(`✗ Error processing ${file.src}:`, error); + process.exit(1); + } + } + + logging.debug('JS processing complete'); +})(); diff --git a/ghost/core/core/app.js b/ghost/core/core/app.js new file mode 100644 index 0000000..ff5d176 --- /dev/null +++ b/ghost/core/core/app.js @@ -0,0 +1,72 @@ +const sentry = require('./shared/sentry'); +const express = require('./shared/express'); +const config = require('./shared/config'); +const logging = require('@tryghost/logging'); +const urlService = require('./server/services/url'); +const fs = require('fs'); +const path = require('path'); +/** @import {Application as ExpressApplication, Request, RequestHandler} from 'express' */ + +/** + * @param {Request} req + * @returns {boolean} + */ +const isMaintenanceModeEnabled = (req) => { + if (req.app.get('maintenance') || config.get('maintenance').enabled || !urlService.hasFinished()) { + return true; + } + + return false; +}; + +/** @type {RequestHandler} */ +const maintenanceMiddleware = function maintenanceMiddleware(req, res, next) { + if (!isMaintenanceModeEnabled(req)) { + return next(); + } + + res.set({ + 'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' + }); + res.writeHead(503, {'content-type': 'text/html'}); + fs.createReadStream(path.resolve(__dirname, './server/views/maintenance.html')).pipe(res); +}; + +/** + * Used by Ghost (Pro) to ensure that requests cannot be served by the wrong site. + * @type {RequestHandler} + */ +const siteIdMiddleware = function siteIdMiddleware(req, res, next) { + const configSiteId = config.get('hostSettings:siteId'); + const headerSiteId = req.headers['x-site-id']; + + if (`${configSiteId}` === `${headerSiteId}`) { + return next(); + } + + logging.warn(`Mismatched site id (expected ${configSiteId}, got ${headerSiteId})`); + + res.set({ + 'Cache-Control': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' + }); + res.writeHead(500); + res.end(); +}; + +/** @returns {ExpressApplication} */ +const rootApp = () => { + const app = express('root'); + app.use(sentry.requestHandler); + if (config.get('sentry')?.tracing?.enabled === true) { + app.use(sentry.tracingHandler); + } + if (config.get('hostSettings:siteId')) { + app.use(siteIdMiddleware); + } + app.enable('maintenance'); + app.use(maintenanceMiddleware); + + return app; +}; + +module.exports = rootApp; diff --git a/ghost/core/core/boot.js b/ghost/core/core/boot.js new file mode 100644 index 0000000..b5482a6 --- /dev/null +++ b/ghost/core/core/boot.js @@ -0,0 +1,614 @@ +// The Ghost Boot Sequence +// ----------------------- +// - This is intentionally one big file at the moment, so that we don't have to follow boot logic all over the place +// - This file is FULL of debug statements so we can see timings for the various steps because the boot needs to be as fast as possible +// - As we manage to break the codebase down into distinct components for e.g. the frontend, their boot logic can be offloaded to them +// - app.js is separate as the first example of each component having it's own app.js file colocated with it, instead of inside of server/web +// +// IMPORTANT: +// ---------- +// The only global requires here should be overrides + debug so we can monitor timings with DEBUG = ghost: boot * node ghost +require('./server/overrides'); +const debug = require('@tryghost/debug')('boot'); +// END OF GLOBAL REQUIRES + +/** + * Helper class to create consistent log messages + */ +class BootLogger { + constructor(logging, metrics, startTime) { + this.logging = logging; + this.metrics = metrics; + this.startTime = startTime; + } + log(message) { + let {logging, startTime} = this; + logging.info(`Ghost ${message} in ${(Date.now() - startTime) / 1000}s`); + } + /** + * @param {string} name + * @param {number} [initialTime] + */ + metric(name, initialTime) { + let {metrics, startTime} = this; + + if (!initialTime) { + initialTime = startTime; + } + + metrics.metric(name, Date.now() - initialTime); + } +} + +/** + * Helper function to handle sending server ready notifications + * @param {string} [error] + */ +function notifyServerReady(error) { + const notify = require('./server/notify'); + + if (error) { + debug('Notifying server ready (error)'); + notify.notifyServerReady(error); + } else { + debug('Notifying server ready (success)'); + notify.notifyServerReady(); + } +} + +/** + * Get the Database into a ready state + * - DatabaseStateManager handles doing all this for us + * + * @param {object} options + * @param {object} options.config + */ +async function initDatabase({config}) { + const DatabaseStateManager = require('./server/data/db/database-state-manager'); + const dbStateManager = new DatabaseStateManager({knexMigratorFilePath: config.get('paths:appRoot')}); + await dbStateManager.makeReady(); + + const databaseInfo = require('./server/data/db/info'); + await databaseInfo.init(); +} + +/** + * Core is intended to be all the bits of Ghost that are fundamental and we can't do anything without them! + * (There's more to do to make this true) + * @param {object} options + * @param {object} options.ghostServer + * @param {object} options.config + * @param {boolean} options.frontend + */ +async function initCore({ghostServer, config, frontend}) { + debug('Begin: initCore'); + + // URL Utils is a bit slow, put it here so the timing is visible separate from models + debug('Begin: Load urlUtils'); + require('./shared/url-utils'); + debug('End: Load urlUtils'); + + // Models are the heart of Ghost - this is a syncronous operation + debug('Begin: models'); + const models = require('./server/models'); + models.init(); + debug('End: models'); + + // Limit service is booted before settings, so that limits are available for calculated settings + debug('Begin: limits'); + const limits = require('./server/services/limits'); + await limits.init(); + debug('End: limits'); + + // Settings are a core concept we use settings to store key-value pairs used in critical pathways as well as public data like the site title + debug('Begin: settings'); + const settings = require('./server/services/settings/settings-service'); + await settings.init(); + await settings.syncEmailSettings(config.get('hostSettings:emailVerification:verified')); + debug('End: settings'); + + debug('Begin: i18n'); + const i18n = require('./server/services/i18n'); + await i18n.init(); + debug('End: i18n'); + + // The URLService is a core part of Ghost, which depends on models. + debug('Begin: Url Service'); + const urlService = require('./server/services/url'); + // Note: there is no await here, we do not wait for the url service to finish + // We can return, but the site will remain in maintenance mode until this finishes + // This is managed on request: https://github.com/TryGhost/Ghost/blob/main/core/app.js#L10 + urlService.init({ + urlCache: !frontend // hacky parameter to make the cache initialization kick in as we can't initialize labs before the boot + }); + debug('End: Url Service'); + + if (ghostServer) { + // Job Service allows parts of Ghost to run in the background + debug('Begin: Job Service'); + const jobService = require('./server/services/jobs'); + + if (config.get('server:testmode')) { + jobService.initTestMode(); + } + + ghostServer.registerCleanupTask(async () => { + await jobService.shutdown(); + }); + debug('End: Job Service'); + + // Mentions Job Service allows mentions to be processed in the background + debug('Begin: Mentions Job Service'); + const mentionsJobService = require('./server/services/mentions-jobs'); + + if (config.get('server:testmode')) { + mentionsJobService.initTestMode(); + } + + ghostServer.registerCleanupTask(async () => { + await mentionsJobService.shutdown(); + }); + debug('End: Mentions Job Service'); + + ghostServer.registerCleanupTask(async () => { + await urlService.shutdown(); + }); + } + + debug('End: initCore'); +} + +/** + * These are services required by Ghost's frontend. + * @param {object} options + * @param {object} options.bootLogger + + */ +async function initServicesForFrontend({bootLogger}) { + debug('Begin: initServicesForFrontend'); + + debug('Begin: Routing Settings'); + const routeSettings = require('./server/services/route-settings'); + await routeSettings.init(); + debug('End: Routing Settings'); + + debug('Begin: Redirects'); + const customRedirects = require('./server/services/custom-redirects'); + await customRedirects.init(); + debug('End: Redirects'); + + debug('Begin: Link Redirects'); + const linkRedirects = require('./server/services/link-redirection'); + await linkRedirects.init(); + debug('End: Link Redirects'); + + debug('Begin: Themes'); + // customThemeSettingsService.api must be initialized before any theme activation occurs + const customThemeSettingsService = require('./server/services/custom-theme-settings'); + customThemeSettingsService.init(); + + const themeService = require('./server/services/themes'); + const themeServiceStart = Date.now(); + await themeService.init(); + bootLogger.metric('theme-service-init', themeServiceStart); + debug('End: Themes'); + + debug('Begin: Offers'); + const offers = require('./server/services/offers'); + await offers.init(); + debug('End: Offers'); + + const frontendDataService = require('./server/services/frontend-data-service'); + let dataService = await frontendDataService.init(); + + debug('End: initServicesForFrontend'); + return {dataService}; +} + +/** + * Frontend is intended to be just Ghost's frontend + */ +async function initFrontend(dataService) { + debug('Begin: initFrontend'); + + const proxyService = require('./frontend/services/proxy'); + proxyService.init({dataService}); + + const helperService = require('./frontend/services/helpers'); + await helperService.init(); + + debug('End: initFrontend'); +} + +/** + * At the moment we load our express apps all in one go, they require themselves and are co-located + * What we want is to be able to optionally load various components and mount them + * So eventually this function should go away + * @param {Object} options + * @param {boolean} options.backend + * @param {boolean} options.frontend + * @param {Object} options.config + */ +async function initExpressApps({frontend, backend, config}) { + debug('Begin: initExpressApps'); + + const parentApp = require('./server/web/parent/app')(); + const vhost = require('@tryghost/mw-vhost'); + + // Mount the express apps on the parentApp + if (backend) { + // ADMIN + API + const backendApp = require('./server/web/parent/backend')(); + parentApp.use(vhost(config.getBackendMountPath(), backendApp)); + } + + if (frontend) { + // SITE + MEMBERS + const urlService = require('./server/services/url'); + const frontendApp = require('./server/web/parent/frontend')({urlService}); + parentApp.use(vhost(config.getFrontendMountPath(), frontendApp)); + } + + debug('End: initExpressApps'); + return parentApp; +} + +/** + * Initialize prometheus client + */ +function initPrometheusClient({config}) { + if (config.get('prometheus:enabled')) { + debug('Begin: initPrometheusClient'); + const prometheusClient = require('./shared/prometheus-client'); + debug('End: initPrometheusClient'); + return prometheusClient; + } + return null; +} + +/** + * Dynamic routing is generated from the routes.yaml file + * When Ghost's DB and core are loaded, we can access this file and call routing.routingManager.start + * However this _must_ happen after the express Apps are loaded, hence why this is here and not in initFrontend + * Routing is currently tightly coupled between the frontend and backend + */ +async function initDynamicRouting() { + debug('Begin: Dynamic Routing'); + const routing = require('./frontend/services/routing'); + const routeSettingsService = require('./server/services/route-settings'); + const bridge = require('./bridge'); + bridge.init(); + + // We pass the dynamic routes here, so that the frontend services are slightly less tightly-coupled + const routeSettings = await routeSettingsService.loadRouteSettings(); + + routing.routerManager.start(routeSettings); + const getRoutesHash = () => routeSettingsService.api.getCurrentHash(); + + const settings = require('./server/services/settings/settings-service'); + await settings.syncRoutesHash(getRoutesHash); + + debug('End: Dynamic Routing'); +} + +/** + * The app service cannot be loaded unless the frontend is enabled + * In future, the logic to determine whether this should be loaded should be in the service loader + */ +async function initAppService() { + debug('Begin: App Service'); + const appService = require('./frontend/services/apps'); + await appService.init(); +} + +/** + * Services are components that make up part of Ghost and need initializing on boot + * These services should all be part of core, frontend services should be loaded with the frontend + * We are working towards this being a service loader, with the ability to make certain services optional + */ +async function initServices() { + debug('Begin: initServices'); + + debug('Begin: Services'); + const identityTokens = require('./server/services/identity-tokens'); + const stripe = require('./server/services/stripe'); + const members = require('./server/services/members'); + const tiers = require('./server/services/tiers'); + const permissions = require('./server/services/permissions'); + const indexnow = require('./server/services/indexnow'); + const slack = require('./server/services/slack'); + const webhooks = require('./server/services/webhooks'); + const postScheduling = require('./server/services/post-scheduling'); + const comments = require('./server/services/comments'); + const staffService = require('./server/services/staff'); + const memberAttribution = require('./server/services/member-attribution'); + const membersEvents = require('./server/services/members-events'); + const linkTracking = require('./server/services/link-tracking'); + const audienceFeedback = require('./server/services/audience-feedback'); + const emailSuppressionList = require('./server/services/email-suppression-list'); + const emailService = require('./server/services/email-service'); + const emailAnalytics = require('./server/services/email-analytics'); + const mentionsService = require('./server/services/mentions'); + const tagsPublic = require('./server/services/tags-public'); + const postsPublic = require('./server/services/posts-public'); + const slackNotifications = require('./server/services/slack-notifications'); + const mediaInliner = require('./server/services/media-inliner'); + const donationService = require('./server/services/donations'); + const giftService = require('./server/services/gifts'); + const recommendationsService = require('./server/services/recommendations'); + const emailAddressService = require('./server/services/email-address'); + const statsService = require('./server/services/stats'); + const explorePingService = require('./server/services/explore-ping'); + + const { + createAdapter: createSchedulerAdapter, + getSchedulerIntegration + } = require('./server/adapters/scheduling/utils'); + const urlUtils = require('./shared/url-utils'); + + // Initialize things that other services depend on first. + const schedulerAdapter = createSchedulerAdapter(); + const [schedulerIntegration] = await Promise.all([ + getSchedulerIntegration(), + stripe.init(), + emailAddressService.init() + ]); + + await Promise.all([ + identityTokens.init(), + memberAttribution.init(), + mentionsService.init(), + staffService.init(), + members.init(), + tiers.init(), + tagsPublic.init(), + postsPublic.init(), + membersEvents.init(), + permissions.init(), + indexnow.listen(), + slack.listen(), + audienceFeedback.init(), + emailService.init(), + emailAnalytics.init(), + webhooks.listen(), + postScheduling.init({ + apiUrl: urlUtils.urlFor('api', {type: 'admin'}, true), + adapter: schedulerAdapter, + integration: schedulerIntegration + }), + comments.init(), + linkTracking.init(), + emailSuppressionList.init(), + slackNotifications.init(), + mediaInliner.init(), + donationService.init(), + recommendationsService.init(), + statsService.init(), + explorePingService.init(), + giftService.init() + ]); + + debug('End: Services'); + + debug('End: initServices'); +} + +/** + * Kick off recurring jobs and background services + * These are things that happen on boot, but we don't need to wait for them to finish + * Later, this might be a service hook + + * @param {object} options + * @param {object} options.config + */ +async function initBackgroundServices({config}) { + debug('Begin: initBackgroundServices'); + + // Load all inactive themes + const themeService = require('./server/services/themes'); + themeService.loadInactiveThemes(); + + // we don't want to kick off background services that will interfere with tests + if (process.env.NODE_ENV.startsWith('test')) { + return; + } + + const activitypub = require('./server/services/activitypub'); + await activitypub.init(); + // Load email analytics recurring jobs + if (config.get('backgroundJobs:emailAnalytics')) { + const emailAnalyticsJobs = require('./server/services/email-analytics/jobs'); + await emailAnalyticsJobs.scheduleRecurringJobs(); + } + + const updateCheck = require('./server/services/update-check'); + updateCheck.scheduleRecurringJobs(); + + const milestonesService = require('./server/services/milestones'); + milestonesService.initAndRun(); + + // TODO(NY-1220): The outbox is deprecated and will soon be removed. + const outboxService = require('./server/services/outbox'); + outboxService.init(); + + const domainEvents = require('@tryghost/domain-events'); + const WelcomeEmailAutomationsService = require('./server/services/welcome-email-automations'); + new WelcomeEmailAutomationsService().init(domainEvents); + + debug('End: initBackgroundServices'); +} + +/** + * ---------------------------------- + * Boot Ghost - The magic starts here + * ---------------------------------- + * + * - This function is written with async/await so you can read, line by line, what happens on boot + * - All the functions above handle init/boot logic for a single component + + * @returns {Promise} ghostServer + */ +async function bootGhost({backend = true, frontend = true, server = true} = {}) { + // Metrics + const startTime = Date.now(); + debug('Begin Boot'); + + // We need access to these variables in both the try and catch block + let bootLogger; + let config; + let ghostServer; + let logging; + let metrics; + + // These require their own try-catch block and error format, because we can't log an error if logging isn't working + try { + // Step 0 - Load config and logging - fundamental required components + // Version is required by logging, sentry & Migration config & so is fundamental to booting + // However, it involves reading package.json so its slow & it's here for visibility on that slowness + debug('Begin: Load version info'); + require('@tryghost/version'); + debug('End: Load version info'); + + // Loading config must be the first thing we do, because it is required for absolutely everything + debug('Begin: Load config'); + config = require('./shared/config'); + debug('End: Load config'); + + // Logging is also used absolutely everywhere + debug('Begin: Load logging'); + logging = require('@tryghost/logging'); + metrics = require('@tryghost/metrics'); + bootLogger = new BootLogger(logging, metrics, startTime); + debug('End: Load logging'); + + // At this point logging is required, so we can handle errors better + + // Add a process handler to capture and log unhandled rejections + debug('Begin: Add unhandled rejection handler'); + process.on('unhandledRejection', (error) => { + logging.error('Unhandled rejection:', error); + }); + debug('End: Add unhandled rejection handler'); + } catch (error) { + console.error(error); // eslint-disable-line no-console + process.exit(1); + } + + try { + // Step 1 - require more fundamental components + + // Sentry must be initialized early, but requires config + debug('Begin: Load sentry'); + const sentry = require('./shared/sentry'); + debug('End: Load sentry'); + + // Initialize prometheus client early to enable metrics collection during boot + // Note: this does not start the metrics server yet to avoid increasing boot time + const prometheusClient = initPrometheusClient({config}); + + // Step 2 - Start server with minimal app in global maintenance mode + debug('Begin: load server + minimal app'); + const rootApp = require('./app')(); + + if (server) { + const GhostServer = require('./server/ghost-server'); + ghostServer = new GhostServer({url: config.getSiteUrl(), env: config.get('env'), serverConfig: config.get('server')}); + await ghostServer.start(rootApp); + bootLogger.log('server started'); + + // Ensure the prometheus client is stopped when the server shuts down + ghostServer.registerCleanupTask(async () => { + if (prometheusClient) { + prometheusClient.stop(); + } + }); + debug('End: load server + minimal app'); + } + + // Step 3 - Get the DB ready + debug('Begin: Get DB ready'); + await initDatabase({config}); + bootLogger.log('database ready'); + const connection = require('./server/data/db/connection'); + sentry.initQueryTracing( + connection + ); + debug('End: Get DB ready'); + + // Step 4 - Load Ghost with all its services + debug('Begin: Load Ghost Services & Apps'); + await initCore({ghostServer, config, frontend}); + + // Instrument the knex instance and connection pool if prometheus is enabled + // Needs to be after initCore because the pool is destroyed and recreated in initCore, which removes the event listeners + if (prometheusClient) { + prometheusClient.instrumentKnex(connection); + } + + const {dataService} = await initServicesForFrontend({bootLogger}); + + if (frontend) { + await initFrontend(dataService); + } + const ghostApp = await initExpressApps({frontend, backend, config}); + + if (frontend) { + await initDynamicRouting(); + await initAppService(); + } + + await initServices(); + debug('End: Load Ghost Services & Apps'); + + // Step 5 - Mount the full Ghost app onto the minimal root app & disable maintenance mode + debug('Begin: mountGhost'); + rootApp.disable('maintenance'); + rootApp.use(config.getSubdir(), ghostApp); + debug('End: mountGhost'); + + // Step 6 - We are technically done here - let everyone know! + bootLogger.log('booted'); + bootLogger.metric('boot-time'); + notifyServerReady(); + + // Step 7 - Init our background services, we don't wait for this to finish + initBackgroundServices({config}); + + // If we pass the env var, kill Ghost + if (process.env.GHOST_CI_SHUTDOWN_AFTER_BOOT) { + process.exit(0); + } + + // We return the server purely for testing purposes + if (server) { + debug('End Boot: Returning Ghost Server'); + return ghostServer; + } else { + debug('End boot: Returning Root App'); + return rootApp; + } + } catch (error) { + const errors = require('@tryghost/errors'); + + // Ensure the error we have is an ignition error + let serverStartError = error; + if (!errors.utils.isGhostError(serverStartError)) { + serverStartError = new errors.InternalServerError({message: serverStartError.message, err: serverStartError}); + } + + logging.error(serverStartError); + + // If ghost was started and something else went wrong, we shut it down + if (ghostServer) { + notifyServerReady(serverStartError); + ghostServer.shutdown(2); + } else { + // Ghost server failed to start, set a timeout to give logging a chance to flush + setTimeout(() => { + process.exit(2); + }, 100); + } + } +} + +module.exports = bootGhost; diff --git a/ghost/core/core/bridge.js b/ghost/core/core/bridge.js new file mode 100644 index 0000000..e000a8b --- /dev/null +++ b/ghost/core/core/bridge.js @@ -0,0 +1,136 @@ +/** + * The Bridge + * + * The bridge is responsible for handing communication from the server to the frontend. + * Data should only be flowing server -> frontend. + * As the architecture improves, the number of cross requires here should go down + * Eventually, the aim is to make this a component that is initialized on boot and is either handed to or actively creates the frontend, if the frontend is desired. + * + * This file is a great place for all the cross-component event handling in lieu of refactoring + * NOTE: You may require anything from shared, the frontend or server here - it is the one place (other than boot) that is allowed :) + */ + +const debug = require('@tryghost/debug')('bridge'); +const errors = require('@tryghost/errors'); +const logging = require('@tryghost/logging'); +const tpl = require('@tryghost/tpl'); +const themeEngine = require('./frontend/services/theme-engine'); +const appService = require('./frontend/services/apps'); +const {adminAuthAssets, cardAssets} = require('./frontend/services/assets-minification'); +const routerManager = require('./frontend/services/routing').routerManager; +const settingsCache = require('./shared/settings-cache'); +const urlService = require('./server/services/url'); +const routeSettings = require('./server/services/route-settings'); +const labs = require('./shared/labs'); + +// Listen to settings.locale.edited, similar to the member service and models/base/listeners +const events = require('./server/lib/common/events'); + +const messages = { + activateFailed: 'Unable to activate the theme "{theme}".' +}; + +class Bridge { + constructor() { + // Track the previous state of the themeTranslation flag + this.previousThemeTranslationState = labs.isSet('themeTranslation'); + } + + init() { + /** + * When locale changes, we reload theme translations + */ + events.on('settings.locale.edited', (model) => { + debug('locale changed, updating i18n to', model.get('value')); + this.getActiveTheme().initI18n({locale: model.get('value')}); + }); + + events.on('settings.labs.edited', () => { + const currentThemeTranslationState = labs.isSet('themeTranslation'); + + if (currentThemeTranslationState !== this.previousThemeTranslationState) { + debug('themeTranslation flag changed from %s to %s', + this.previousThemeTranslationState, currentThemeTranslationState); + const locale = settingsCache.get('locale'); + this.getActiveTheme().initI18n({locale}); + + // Update the tracked state + this.previousThemeTranslationState = currentThemeTranslationState; + } + }); + + // NOTE: eventually this event should somehow be listened on and handled by the URL Service + // for now this eliminates the need for the frontend routing to listen to + // server events + events.on('settings.timezone.edited', (model) => { + routerManager.handleTimezoneEdit(model); + }); + } + + getActiveTheme() { + return themeEngine.getActive(); + } + + ensureAdminAuthAssetsMiddleware() { + return adminAuthAssets.serveMiddleware(); + } + + async activateTheme(loadedTheme, checkedTheme) { + let settings = { + locale: settingsCache.get('locale') + }; + // no need to check the score, activation should be used in combination with validate.check + // Use the two theme objects to set the current active theme + try { + themeEngine.setActive(settings, loadedTheme, checkedTheme); + + logging.info('Invalidating assets for regeneration'); + + const cardAssetConfig = this.getCardAssetConfig(); + debug('reload card assets config', cardAssetConfig); + cardAssets.invalidate(cardAssetConfig); + + // rebuild asset files + adminAuthAssets.invalidate(); + } catch (err) { + logging.error(new errors.InternalServerError({ + message: tpl(messages.activateFailed, {theme: loadedTheme.name}), + err: err + })); + } + } + + getCardAssetConfig() { + if (this.getActiveTheme()) { + return this.getActiveTheme().config('card_assets'); + } else { + return true; + } + } + + async reloadFrontend() { + debug('reload frontend'); + const siteApp = require('./frontend/web/site'); + + const routerConfig = { + routeSettings: await routeSettings.loadRouteSettings(), + urlService + }; + + await siteApp.reload(routerConfig); + + // re-initialize apps (register app routers, because we have re-initialized the site routers) + appService.init(); + + // connect routers and resources again + urlService.queue.start({ + event: 'init', + tolerance: 100, + requiredSubscriberCount: 1 + }); + } +} + +const bridge = new Bridge(); + +module.exports = bridge; \ No newline at end of file diff --git a/ghost/core/ghost.js b/ghost/core/ghost.js new file mode 100644 index 0000000..74a27d8 --- /dev/null +++ b/ghost/core/ghost.js @@ -0,0 +1,25 @@ +/** + * Internal CLI Placeholder + * + * If we want to add alternative commands, flags, or modify environment vars, it should all go here. + * Important: This file should not contain any requires, unless we decide to add pretty-cli/commander type tools + * + **/ + +// Don't allow NODE_ENV to be null +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +const argv = process.argv; +const mode = argv[2]; + +// Switch between boot modes +switch (mode) { +case 'repl': +case 'timetravel': +case 'generate-data': + require('./core/cli/command').run(mode); + break; +default: + // New boot sequence + require('./core/boot')(); +} diff --git a/ghost/core/index.js b/ghost/core/index.js new file mode 100644 index 0000000..3eb69a6 --- /dev/null +++ b/ghost/core/index.js @@ -0,0 +1 @@ +require('./ghost'); diff --git a/ghost/core/jsconfig.json b/ghost/core/jsconfig.json new file mode 100644 index 0000000..cf1201c --- /dev/null +++ b/ghost/core/jsconfig.json @@ -0,0 +1,12 @@ +{ + "include": ["core/**/*.js", "test/**/*.js"], + "compilerOptions": { + "checkJs": true, + "module": "commonjs", + "target": "es2018", + "moduleResolution": "node" + }, + "exclude": [ + "core/built" + ] +} diff --git a/ghost/core/loggingrc.js b/ghost/core/loggingrc.js new file mode 100644 index 0000000..a91bd3c --- /dev/null +++ b/ghost/core/loggingrc.js @@ -0,0 +1,27 @@ +const config = require('./core/shared/config'); +const ghostVersion = require('@tryghost/version'); + +// Config for logging +const loggingConfig = config.get('logging') || {}; + +if (!loggingConfig.path) { + loggingConfig.path = config.getContentPath('logs'); +} + +// Additional values used by logging +loggingConfig.env = config.get('env'); +loggingConfig.domain = config.get('url'); +loggingConfig.metadata = { + version: ghostVersion.original +}; + +// Config for metrics +loggingConfig.metrics = config.get('logging:metrics') || {}; +loggingConfig.metrics.metadata = { + // Undefined if unavailable + siteId: config.get('hostSettings:siteId'), + domain: config.get('url'), + version: ghostVersion.original +}; + +module.exports = loggingConfig; diff --git a/ghost/core/monobundle.js b/ghost/core/monobundle.js new file mode 100755 index 0000000..dbfdf9e --- /dev/null +++ b/ghost/core/monobundle.js @@ -0,0 +1,235 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ + +const fs = require('fs'); +const path = require('path'); + +const concurrently = require('concurrently'); +const detectIndent = require('detect-indent'); +const detectNewline = require('detect-newline'); +const findRoot = require('find-root'); +const {flattenDeep} = require('lodash'); +const glob = require('glob'); + +const DETECT_TRAILING_WHITESPACE = /\s+$/; + +const jsonFiles = new Map(); + +class JSONFile { + /** + * @param {string} filePath + * @returns {JSONFile} + */ + static for(filePath) { + if (jsonFiles.has(filePath)) { + return jsonFiles.get(filePath); + } + + let jsonFile = new this(filePath); + jsonFiles.set(filePath, jsonFile); + + return jsonFile; + } + + /** + * @param {string} filename + */ + constructor(filename) { + this.filename = filename; + this.reload(); + } + + reload() { + const contents = fs.readFileSync(this.filename, {encoding: 'utf8'}); + + this.pkg = JSON.parse(contents); + this.lineEndings = detectNewline(contents); + this.indent = detectIndent(contents).amount; + + let trailingWhitespace = DETECT_TRAILING_WHITESPACE.exec(contents); + this.trailingWhitespace = trailingWhitespace ? trailingWhitespace : ''; + } + + write() { + let contents = JSON.stringify(this.pkg, null, this.indent).replace(/\n/g, this.lineEndings); + + fs.writeFileSync(this.filename, contents + this.trailingWhitespace, {encoding: 'utf8'}); + } +} + +/** + * Read workspace package globs from pnpm-workspace.yaml. + * @param {string} dir + * @returns {string[]|null} + */ +function getPackages(dir) { + const pnpmWorkspace = path.join(dir, 'pnpm-workspace.yaml'); + if (!fs.existsSync(pnpmWorkspace)) { + return null; + } + + const content = fs.readFileSync(pnpmWorkspace, 'utf8'); + const packages = []; + let inPackages = false; + for (const line of content.split('\n')) { + if (line.startsWith('packages:')) { + inPackages = true; + continue; + } + if (inPackages) { + const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/); + if (match) { + packages.push(match[1]); + } else if (/^\S/.test(line)) { + break; + } + } + } + + return packages.length > 0 ? packages : null; +} + +/** + * @param {string} from + * @returns {string[]} + */ +function getWorkspaces(from) { + const root = findRoot(from, (dir) => { + return getPackages(dir) !== null; + }); + + const packages = getPackages(root); + return flattenDeep(packages.map(name => glob.sync(path.join(root, `${name}/`)))); +} + +(async () => { + const cwd = process.cwd(); + const nearestPkgJson = findRoot(cwd); + console.log('nearestPkgJson', nearestPkgJson); + const pkgInfo = JSONFile.for(path.join(nearestPkgJson, 'package.json')); + + if (pkgInfo.pkg.name !== 'ghost') { + console.log('This script must be run from the `ghost` npm package directory'); + process.exit(1); + } + + const bundlePath = './components'; + if (!fs.existsSync(bundlePath)){ + fs.mkdirSync(bundlePath); + } + + const workspaces = getWorkspaces(cwd) + .filter(w => !w.startsWith(cwd) && fs.existsSync(path.join(w, 'package.json'))) + .filter(w => !w.includes('apps/')) + .filter(w => !w.includes('/admin/')) + .filter(w => !w.includes('/e2e/')); + + console.log('workspaces', workspaces); + console.log('\n-------------------------\n'); + + const packagesToPack = []; + + for (const w of workspaces) { + const workspacePkgInfo = JSONFile.for(path.join(w, 'package.json')); + + if (!workspacePkgInfo.pkg.private) { + continue; + } + + workspacePkgInfo.pkg.version = pkgInfo.pkg.version; + workspacePkgInfo.write(); + + const slugifiedName = workspacePkgInfo.pkg.name.replace(/@/g, '').replace(/\//g, '-'); + const packedFilename = `file:` + path.join(bundlePath, `${slugifiedName}-${workspacePkgInfo.pkg.version}.tgz`); + + if (pkgInfo.pkg.dependencies[workspacePkgInfo.pkg.name]) { + console.log(`[${workspacePkgInfo.pkg.name}] dependencies override => ${packedFilename}`); + pkgInfo.pkg.dependencies[workspacePkgInfo.pkg.name] = packedFilename; + } + + if (pkgInfo.pkg.devDependencies[workspacePkgInfo.pkg.name]) { + console.log(`[${workspacePkgInfo.pkg.name}] devDependencies override => ${packedFilename}`); + pkgInfo.pkg.devDependencies[workspacePkgInfo.pkg.name] = packedFilename; + } + + if (pkgInfo.pkg.optionalDependencies[workspacePkgInfo.pkg.name]) { + console.log(`[${workspacePkgInfo.pkg.name}] optionalDependencies override => ${packedFilename}`); + pkgInfo.pkg.optionalDependencies[workspacePkgInfo.pkg.name] = packedFilename; + } + + console.log(`[${workspacePkgInfo.pkg.name}] resolution override => ${packedFilename}\n`); + if (!pkgInfo.pkg.resolutions) { + pkgInfo.pkg.resolutions = {}; + } + pkgInfo.pkg.resolutions[workspacePkgInfo.pkg.name] = packedFilename; + + packagesToPack.push(w); + } + + // Copy pnpm.overrides from root package.json so production installs + // pin the same transitive dependency versions as the workspace. + const rootPkgPath = path.join(findRoot(path.dirname(nearestPkgJson)), 'package.json'); + const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8')); + if (rootPkg.pnpm?.overrides) { + const workspaceNames = new Set(workspaces + .map((w) => { + const wpkg = path.join(w, 'package.json'); + return fs.existsSync(wpkg) ? JSON.parse(fs.readFileSync(wpkg, 'utf8')).name : null; + }) + .filter(Boolean)); + + const filteredOverrides = {}; + for (const [key, value] of Object.entries(rootPkg.pnpm.overrides)) { + if (!workspaceNames.has(key)) { + filteredOverrides[key] = value; + } + } + + if (!pkgInfo.pkg.pnpm) { + pkgInfo.pkg.pnpm = {}; + } + pkgInfo.pkg.pnpm.overrides = {...filteredOverrides, ...pkgInfo.pkg.pnpm.overrides}; + } + + // Copy onlyBuiltDependencies so native addons can run install scripts. + if (rootPkg.pnpm?.onlyBuiltDependencies) { + if (!pkgInfo.pkg.pnpm) { + pkgInfo.pkg.pnpm = {}; + } + pkgInfo.pkg.pnpm.onlyBuiltDependencies = rootPkg.pnpm.onlyBuiltDependencies; + } + + // Copy packageManager so corepack uses the correct pnpm version. + if (rootPkg.packageManager) { + pkgInfo.pkg.packageManager = rootPkg.packageManager; + } + + pkgInfo.write(); + + const {result} = concurrently(packagesToPack.map(w => ({ + name: w, + cwd: w, + command: 'npm pack --pack-destination ../core/components' + }))); + + try { + await result; + } catch (e) { + console.error(e); + throw e; + } + + const filesToCopy = [ + 'README.md', + 'LICENSE', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + '.npmrc' + ]; + + for (const file of filesToCopy) { + console.log(`copying ../../${file} to ${file}`); + fs.copyFileSync(path.join('../../', file), file); + } +})(); diff --git a/ghost/core/nodemon.json b/ghost/core/nodemon.json new file mode 100644 index 0000000..f5c985a --- /dev/null +++ b/ghost/core/nodemon.json @@ -0,0 +1,13 @@ +{ + "watch": [ + ".", + "../*" + ], + "ignore": [ + "../admin/**", + "content/**", + "core/built/**" + ], + "ext": "js,mjs,cjs,json,ts,tsx", + "exec": "node --import=tsx" +} diff --git a/ghost/core/package.json b/ghost/core/package.json new file mode 100644 index 0000000..340f96e --- /dev/null +++ b/ghost/core/package.json @@ -0,0 +1,346 @@ +{ + "name": "ghost", + "version": "6.33.0-rc.0", + "description": "The professional publishing platform", + "author": "Ghost Foundation", + "homepage": "https://ghost.org", + "keywords": [ + "ghost", + "blog", + "cms", + "headless", + "content", + "markdown" + ], + "repository": { + "type": "git", + "url": "git://github.com/TryGhost/Ghost.git" + }, + "bugs": "https://github.com/TryGhost/Ghost/issues", + "contributors": "https://github.com/TryGhost/Ghost/graphs/contributors", + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "archive": "pnpm pack:standalone && pnpm pack:tarball", + "pack:standalone": "rm -rf package; find . -maxdepth 1 -name 'ghost-*.tgz' -delete; npm pack && tar xzf ghost-*.tgz && cp ../../pnpm-lock.yaml package/ && cp ../../.npmrc package/.npmrc && echo '\nfrozen-lockfile=false\nshamefully-hoist=true' >> package/.npmrc && rm ghost-*.tgz && rm -f pnpm-lock.yaml pnpm-workspace.yaml .npmrc", + "pack:tarball": "tar czf ghost-$(node -p \"require('./package.json').version\").tgz package", + "dev": "nodemon index.js", + "build:assets": "pnpm build:assets:css && pnpm build:assets:js", + "build:assets:js": "node bin/minify-assets.js", + "generate-golden-email": "node bin/generate-golden-email.js", + "migrate:create": "node bin/create-migration.js", + "build:assets:css": "postcss core/frontend/public/ghost.css --no-map --use cssnano -o core/frontend/public/ghost.min.css", + "build:tsc": "tsc", + "pretest": "pnpm build:assets", + "test": "pnpm test:unit", + "test:base": "mocha --reporter dot --node-option import=tsx --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js,test.ts", + "test:single": "f() { q=$(echo \"$1\" | sed 's/\\.test\\.[jt]s$//'); case \"$q\" in */*) pnpm test:base --timeout=60000 \"$1\" ;; *) pnpm test:base --timeout=60000 \"test/**/*$q*.test.{js,ts}\" ;; esac; }; f", + "test:all": "pnpm test:unit && pnpm test:integration && pnpm test:e2e && pnpm lint", + "test:debug": "DEBUG=ghost:test* pnpm test", + "test:unit": "c8 pnpm test:unit:${GHOST_UNIT_TEST_VARIANT:-base}", + "test:unit:base": "pnpm test:base './test/unit' --timeout=2000", + "test:unit:ci": "pnpm test:unit:base --reporter=min", + "test:integration": "pnpm test:base './test/integration' --timeout=10000", + "test:e2e": "pnpm test:base ./test/e2e-* --timeout=15000", + "test:legacy": "pnpm test:base './test/legacy' --timeout=60000", + "test:ci:e2e": "c8 -c ./.c8rc.e2e.json -o coverage-e2e pnpm test:e2e -b", + "test:ci:legacy": "pnpm test:legacy -b", + "test:ci:integration": "c8 -c ./.c8rc.e2e.json -o coverage-integration --lines 52 --functions 47 --branches 73 --statements 52 pnpm test:integration -b", + "test:unit:slow": "pnpm test:unit --reporter=mocha-slow-test-reporter", + "test:int:slow": "pnpm test:integration --reporter=mocha-slow-test-reporter", + "test:e2e:slow": "pnpm test:e2e --reporter=mocha-slow-test-reporter", + "test:leg:slow": "mocha --reporter dot --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js './test/legacy' --timeout=60000 --reporter=mocha-slow-test-reporter", + "lint:server": "eslint --ignore-path .eslintignore 'core/server/**/*.js' 'core/*.js' '*.js' --cache", + "lint:shared": "eslint --ignore-path .eslintignore 'core/shared/**/*.js' --cache", + "lint:frontend": "eslint --ignore-path .eslintignore 'core/frontend/**/*.js' --cache", + "lint:test": "eslint -c test/.eslintrc.js --ignore-path test/.eslintignore 'test/**/*.js' --cache", + "lint:code": "pnpm lint:server && pnpm lint:shared && pnpm lint:frontend", + "lint:types": "eslint --ignore-path .eslintignore '**/*.ts' --cache && tsc --noEmit", + "lint": "pnpm lint:server && pnpm lint:shared && pnpm lint:frontend && pnpm lint:test && pnpm lint:types", + "prepack": "node monobundle.js", + "postpack": "cd ../.. && git checkout -- ghost/core/package.json ghost/i18n/package.json ghost/parse-email-address/package.json && rm -f ghost/core/pnpm-lock.yaml ghost/core/pnpm-workspace.yaml ghost/core/.npmrc && rm -rf ghost/core/components" + }, + "engines": { + "node": "^22.13.1", + "cli": "^1.29.1" + }, + "dependencies": { + "@aws-sdk/client-s3": "3.1025.0", + "@extractus/oembed-extractor": "3.2.1", + "@faker-js/faker": "7.6.0", + "@isaacs/ttlcache": "1.4.1", + "@sentry/node": "7.120.4", + "@slack/webhook": "7.0.9", + "@tryghost/adapter-base-cache": "0.1.23", + "@tryghost/admin-api-schema": "4.7.2", + "@tryghost/api-framework": "1.0.7", + "@tryghost/bookshelf-plugins": "2.0.3", + "@tryghost/color-utils": "0.2.16", + "@tryghost/config-url-helpers": "1.0.23", + "@tryghost/custom-fonts": "1.0.8", + "@tryghost/database-info": "0.3.35", + "@tryghost/debug": "0.1.40", + "@tryghost/domain-events": "1.0.8", + "@tryghost/email-mock-receiver": "0.3.16", + "@tryghost/errors": "1.3.13", + "@tryghost/helpers": "1.1.103", + "@tryghost/html-to-plaintext": "1.0.8", + "@tryghost/http-cache-utils": "0.1.25", + "@tryghost/i18n": "workspace:*", + "@tryghost/image-transform": "1.4.13", + "@tryghost/job-manager": "1.0.9", + "@tryghost/kg-card-factory": "5.1.14", + "@tryghost/kg-clean-basic-html": "4.2.23", + "@tryghost/kg-converters": "1.1.21", + "@tryghost/kg-default-atoms": "5.1.9", + "@tryghost/kg-default-cards": "10.2.13", + "@tryghost/kg-default-nodes": "2.0.21", + "@tryghost/kg-default-transforms": "1.2.44", + "@tryghost/kg-html-to-lexical": "1.2.45", + "@tryghost/kg-lexical-html-renderer": "1.3.44", + "@tryghost/kg-markdown-html-renderer": "7.1.18", + "@tryghost/kg-mobiledoc-html-renderer": "7.1.18", + "@tryghost/limit-service": "1.5.2", + "@tryghost/logging": "2.5.5", + "@tryghost/members-csv": "2.0.5", + "@tryghost/metrics": "1.0.43", + "@tryghost/mongo-utils": "0.6.3", + "@tryghost/mw-error-handler": "1.0.13", + "@tryghost/mw-vhost": "1.0.6", + "@tryghost/nodemailer": "0.3.48", + "@tryghost/nql": "0.12.10", + "@tryghost/nql-lang": "0.6.4", + "@tryghost/parse-email-address": "workspace:*", + "@tryghost/pretty-cli": "1.2.52", + "@tryghost/prometheus-metrics": "1.0.8", + "@tryghost/promise": "0.3.20", + "@tryghost/referrer-parser": "0.1.15", + "@tryghost/request": "1.0.12", + "@tryghost/root-utils": "0.3.38", + "@tryghost/security": "1.0.6", + "@tryghost/social-urls": "0.1.60", + "@tryghost/string": "0.3.2", + "@tryghost/tpl": "0.1.40", + "@tryghost/url-utils": "5.1.2", + "@tryghost/validator": "0.2.22", + "@tryghost/version": "0.1.38", + "@tryghost/zip": "1.1.54", + "body-parser": "1.20.4", + "bookshelf": "1.2.0", + "bookshelf-relations": "2.8.0", + "brute-knex": "4.0.1", + "bson-objectid": "2.0.4", + "cache-manager": "4.1.0", + "cache-manager-ioredis": "2.1.0", + "chalk": "4.1.2", + "charset": "1.0.1", + "cheerio": "0.22.0", + "clsx": "2.1.1", + "cluster-key-slot": "1.1.2", + "common-tags": "1.8.2", + "compression": "1.8.1", + "connect-slashes": "1.4.0", + "cookie-session": "2.1.1", + "cookies": "0.9.1", + "cors": "2.8.6", + "countries-and-timezones": "3.8.0", + "csso": "5.0.5", + "csv-writer": "1.6.0", + "date-fns": "2.30.0", + "dompurify": "3.3.0", + "downsize": "0.0.8", + "entities": "4.5.0", + "express": "4.21.2", + "express-brute": "1.0.1", + "express-hbs": "2.5.0", + "express-jwt": "8.5.1", + "express-lazy-router": "1.0.6", + "express-query-boolean": "2.0.0", + "express-queue": "0.0.13", + "express-session": "1.19.0", + "file-type": "16.5.4", + "form-data": "4.0.5", + "fs-extra": "11.3.4", + "ghost-storage-base": "1.1.2", + "glob": "8.1.0", + "got": "13.0.0", + "gscan": "5.4.3", + "handlebars": "4.7.9", + "heic-convert": "2.1.0", + "html-to-text": "5.1.1", + "html5parser": "2.0.2", + "human-number": "2.0.10", + "iconv-lite": "0.7.2", + "image-size": "1.2.1", + "intl": "1.2.5", + "intl-messageformat": "5.4.3", + "js-yaml": "4.1.1", + "jsdom": "28.1.0", + "jsonc-parser": "3.3.1", + "jsonwebtoken": "8.5.1", + "juice": "9.1.0", + "keypair": "1.0.4", + "knex": "2.4.2", + "knex-migrator": "5.3.2", + "leaky-bucket": "2.2.0", + "lodash": "4.17.23", + "luxon": "3.7.2", + "mailgun.js": "10.4.0", + "metascraper": "5.45.15", + "metascraper-author": "5.45.10", + "metascraper-description": "5.45.10", + "metascraper-image": "5.45.10", + "metascraper-logo": "5.45.10", + "metascraper-logo-favicon": "5.42.0", + "metascraper-publisher": "5.45.10", + "metascraper-title": "5.45.10", + "metascraper-url": "5.45.10", + "mime-types": "2.1.35", + "mingo": "2.5.3", + "moment": "2.24.0", + "moment-timezone": "0.5.45", + "multer": "2.0.2", + "mysql2": "3.18.1", + "nconf": "0.13.0", + "node-fetch": "2.7.0", + "node-jose": "2.2.0", + "nodemailer": "6.10.1", + "on-headers": "^1.1.0", + "otplib": "12.0.1", + "papaparse": "5.5.3", + "path-match": "1.2.4", + "probability-distributions": "0.9.1", + "probe-image-size": "7.2.3", + "rss": "1.2.2", + "sanitize-html": "2.17.0", + "semver": "7.7.4", + "simple-dom": "1.4.0", + "stoppable": "1.1.0", + "stripe": "8.222.0", + "superagent": "5.3.1", + "superagent-throttle": "1.0.1", + "terser": "5.46.1", + "tiny-glob": "0.2.9", + "ua-parser-js": "1.0.41", + "xml": "1.0.1" + }, + "overrides": { + "lodash.template": "4.5.0" + }, + "optionalDependencies": { + "@tryghost/html-to-mobiledoc": "3.2.26", + "sqlite3": "5.1.7" + }, + "devDependencies": { + "@actions/core": "3.0.0", + "@prettier/sync": "0.6.1", + "@tryghost/express-test": "0.15.5", + "@tryghost/webhook-mock-receiver": "0.2.22", + "@types/bookshelf": "1.2.9", + "@types/common-tags": "1.8.4", + "@types/express": "4.17.25", + "@types/jsdom": "28.0.1", + "@types/jsonwebtoken": "9.0.10", + "@types/lodash": "4.17.24", + "@types/lodash-es": "4.17.12", + "@types/mime-types": "3.0.1", + "@types/mocha": "10.0.10", + "@types/node": "22.19.17", + "@types/node-fetch": "2.6.13", + "@types/node-jose": "1.1.13", + "@types/nodemailer": "6.4.23", + "@types/on-headers": "1.0.4", + "@types/sinon": "17.0.4", + "@types/supertest": "6.0.3", + "c8": "10.1.3", + "cli-progress": "3.12.0", + "cssnano": "7.1.1", + "detect-indent": "6.1.0", + "detect-newline": "3.1.0", + "expect": "29.7.0", + "find-root": "1.1.0", + "form-data": "4.0.5", + "html-minifier": "4.0.0", + "html-validate": "8.29.0", + "inquirer": "8.2.7", + "jwk-to-pem": "2.0.7", + "jwks-rsa": "3.2.0", + "lodash-es": "4.17.23", + "mocha": "11.7.5", + "mocha-slow-test-reporter": "0.1.2", + "mock-knex": "TryGhost/mock-knex#68948e11b0ea4fe63456098dfdc169bea7f62009", + "nock": "13.5.6", + "nodemon": "3.1.14", + "papaparse": "5.5.3", + "parse-prometheus-text-format": "1.1.1", + "postcss": "8.5.6", + "postcss-cli": "11.0.1", + "rewire": "9.0.1", + "sinon": "18.0.1", + "supertest": "6.3.4", + "tmp": "0.2.5", + "toml": "3.0.0", + "tsx": "4.21.0", + "typescript": "5.9.3" + }, + "nx": { + "targets": { + "build:tsc": { + "dependsOn": [ + "^build" + ] + }, + "archive": { + "dependsOn": [ + "build:assets", + "build:tsc", + { + "projects": [ + "@tryghost/admin" + ], + "target": "build" + } + ] + }, + "dev": { + "dependsOn": [ + "build:assets" + ] + }, + "lint": { + "dependsOn": [ + "^build" + ] + }, + "test:all": { + "dependsOn": [ + "build:assets" + ] + }, + "test:unit": { + "dependsOn": [ + "build:assets", + "^build" + ] + }, + "test:ci:e2e": { + "dependsOn": [ + "^build" + ] + }, + "test:ci:legacy": { + "dependsOn": [ + "^build" + ] + }, + "test:ci:integration": { + "dependsOn": [ + "^build" + ] + } + } + } +} diff --git a/ghost/core/test/.eslintignore b/ghost/core/test/.eslintignore new file mode 100644 index 0000000..300ac9f --- /dev/null +++ b/ghost/core/test/.eslintignore @@ -0,0 +1,3 @@ +test/coverage/** +test/utils/fixtures/themes/casper/assets/** +test/utils/fixtures/themes/source/assets/** diff --git a/ghost/core/test/.eslintrc.js b/ghost/core/test/.eslintrc.js new file mode 100644 index 0000000..8a804f7 --- /dev/null +++ b/ghost/core/test/.eslintrc.js @@ -0,0 +1,47 @@ +module.exports = { + env: { + es6: true, + node: true, + mocha: true + }, + plugins: [ + 'ghost' + ], + extends: [ + 'eslint:recommended', + 'plugin:ghost/test' + ], + overrides: [ + { + files: ['**/*.ts'], + parser: '@typescript-eslint/parser', + extends: [ + 'plugin:ghost/test' + ] + } + ], + rules: { + // TODO: remove this rule once it's turned into "error" in the base plugin + 'no-shadow': 'error', + + // these rules were were not previously enforced in our custom rules, + // they're turned off here because they _are_ enforced in our plugin. + // TODO: remove these custom rules and fix the problems in test files where appropriate + camelcase: 'off', + 'no-prototype-builtins': 'off', + 'no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^should$' + } + ], + 'no-useless-escape': 'off', + + 'ghost/mocha/no-skipped-tests': 'error', + 'ghost/filenames/match-regex': ['error', '^[a-z0-9-.]+$', null, true], + + // TODO: remove these custom rules and fix problems in test files + 'ghost/mocha/no-setup-in-describe': 'off', + 'ghost/mocha/no-sibling-hooks': 'off' + } +}; diff --git a/ghost/core/tsconfig.json b/ghost/core/tsconfig.json new file mode 100644 index 0000000..795d757 --- /dev/null +++ b/ghost/core/tsconfig.json @@ -0,0 +1,107 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + //"rootDir": "src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["node", "mocha"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + //"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + //"outDir": "build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": [ + "core/**/*.ts", + "test/**/*.ts", + "types/**/*.d.ts" + ] +} diff --git a/ghost/core/types/ghost-storage-base.d.ts b/ghost/core/types/ghost-storage-base.d.ts new file mode 100644 index 0000000..d4149fd --- /dev/null +++ b/ghost/core/types/ghost-storage-base.d.ts @@ -0,0 +1,22 @@ +declare module 'ghost-storage-base' { + import {RequestHandler} from 'express'; + + export type StorageFile = { + name: string; + path: string; + type?: string; + }; + + export default abstract class StorageBase { + protected storagePath: string; + getTargetDir(baseDir?: string): string; + getUniqueFileName(file: StorageFile, targetDir: string): Promise; + abstract save(file: StorageFile, targetDir?: string): Promise; + abstract saveRaw(buffer: Buffer, targetPath: string): Promise; + abstract exists(fileName: string, targetDir: string): Promise; + abstract delete(fileName: string, targetDir: string): Promise; + abstract read(file: {path: string}): Promise; + abstract serve(): RequestHandler; + abstract urlToPath(url: string): string; + } +} diff --git a/ghost/i18n/.eslintrc.js b/ghost/i18n/.eslintrc.js new file mode 100644 index 0000000..d876c73 --- /dev/null +++ b/ghost/i18n/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ], + rules: { + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/ghost/i18n/README.md b/ghost/i18n/README.md new file mode 100644 index 0000000..9b95ed1 --- /dev/null +++ b/ghost/i18n/README.md @@ -0,0 +1,17 @@ +# i18n + +i18n translations for Ghost + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +## Test + +- `pnpm lint` run just eslint +- `pnpm test` run lint and tests + diff --git a/ghost/i18n/generate-context.js b/ghost/i18n/generate-context.js new file mode 100644 index 0000000..bf99246 --- /dev/null +++ b/ghost/i18n/generate-context.js @@ -0,0 +1,57 @@ +const fs = require('fs').promises; +const path = require('path'); + +const BASE_PATH = './locales/en'; +const CONTEXT_FILE = './locales/context.json'; + +(async () => { + const existingContent = await fs.readFile(CONTEXT_FILE, 'utf-8'); + const context = JSON.parse(existingContent); + + const newContext = {}; + + const files = await fs.readdir(BASE_PATH); + + for (const file of files) { + const filePath = path.join(process.cwd(), BASE_PATH, file); + const data = require(filePath); + + for (const key of Object.keys(data)) { + newContext[key] = context[key] || ''; + } + } + + const orderedContext = Object.keys(newContext).sort().reduce((obj, key) => { + obj[key] = newContext[key]; + return obj; + }, {}); + + const newContent = JSON.stringify(orderedContext, null, 4); + + if (process.env.CI && newContent !== existingContent) { + // eslint-disable-next-line no-console + console.error('context.json is out of date. Run `pnpm translate` in ghost/i18n and commit the result.'); + process.exit(1); + } + + const emptyKeys = Object.keys(orderedContext).filter((key) => { + const value = orderedContext[key] ?? ''; + return value.trim() === ''; + }); + + if (emptyKeys.length > 0) { + if (process.env.CI) { + const keyList = emptyKeys.map(k => ' - "' + k + '"').join('\n'); + // eslint-disable-next-line no-console + console.error('Translation keys are missing context descriptions in context.json:\n' + keyList); + // eslint-disable-next-line no-console + console.error('\nAdd a description for each key in locales/context.json to help translators understand where and how the string is used.'); + process.exit(1); + } else { + // eslint-disable-next-line no-console + console.warn(`Warning: ${emptyKeys.length} key(s) in context.json have empty descriptions. Please add context before committing.`); + } + } + + await fs.writeFile(CONTEXT_FILE, newContent); +})(); diff --git a/ghost/i18n/i18next-parser.config.js b/ghost/i18n/i18next-parser.config.js new file mode 100644 index 0000000..02be894 --- /dev/null +++ b/ghost/i18n/i18next-parser.config.js @@ -0,0 +1,21 @@ +const {SUPPORTED_LOCALES} = require('./'); + +/** + * @type {import('i18next-parser').UserConfig} + */ +module.exports = { + locales: SUPPORTED_LOCALES, + + keySeparator: false, + namespaceSeparator: false, + + defaultNamespace: process.env.NAMESPACE || 'translation', + + createOldCatalogs: false, + indentation: 4, + sort: true, + + failOnUpdate: process.env.CI, + + output: 'locales/$LOCALE/$NAMESPACE.json' +}; diff --git a/ghost/i18n/index.js b/ghost/i18n/index.js new file mode 100644 index 0000000..817b127 --- /dev/null +++ b/ghost/i18n/index.js @@ -0,0 +1,8 @@ +const i18n = require('./lib/i18n'); + +// Explicit exports for better bundler compatibility +module.exports = i18n; +module.exports.default = i18n; +module.exports.LOCALE_DATA = i18n.LOCALE_DATA; +module.exports.SUPPORTED_LOCALES = i18n.SUPPORTED_LOCALES; +module.exports.generateResources = i18n.generateResources; diff --git a/ghost/i18n/lib/i18n.js b/ghost/i18n/lib/i18n.js new file mode 100644 index 0000000..dea7bf5 --- /dev/null +++ b/ghost/i18n/lib/i18n.js @@ -0,0 +1,155 @@ +const i18next = require('i18next'); +const path = require('path'); +const fs = require('fs'); +const debug = require('@tryghost/debug')('i18n'); + +// Locale data loaded from JSON (single source of truth) +const LOCALE_DATA = require('./locale-data.json'); + +// Export just the locale codes for backward compatibility +const SUPPORTED_LOCALES = LOCALE_DATA.map(locale => locale.code); + +function generateResources(locales, ns) { + return locales.reduce((acc, locale) => { + let res; + // add an extra fallback - this handles the case where we have a partial set of translations for some reason + // by falling back to the english translations + try { + res = require(`../locales/${locale}/${ns}.json`); + } catch (err) { + res = require(`../locales/en/${ns}.json`); + } + + // Note: due some random thing in TypeScript, 'requiring' a JSON file with a space in a key name, only adds it to the default export + // If changing this behaviour, please also check the comments and signup-form apps in another language (mainly sentences with a space in them) + acc[locale] = { + [ns]: {...res, ...(res.default && typeof res.default === 'object' ? res.default : {})} + }; + return acc; + }, {}); +} + +function generateThemeResources(lng, themeLocalesPath) { + if (!themeLocalesPath) { + return { + [lng]: { + theme: {} + } + }; + } + + // Get available theme locales by scanning the directory + let availableLocales = []; + try { + const files = fs.readdirSync(themeLocalesPath); + availableLocales = files + .filter(file => file.endsWith('.json')) + .map(file => file.replace('.json', '')); + } catch (err) { + // If we can't read the directory, fall back to just trying the requested locale and English + + availableLocales = [lng, 'en']; + } + + // Always include the requested locale and English as fallbacks + const locales = [...new Set([lng, ...availableLocales, 'en'])]; + + return locales.reduce((acc, locale) => { + let res; + let needsFallback = false; + // Try to load the locale file, fallback to English + const localePath = path.join(themeLocalesPath, `${locale}.json`); + if (fs.existsSync(localePath)) { + try { + // Delete from require cache to ensure fresh reads for theme files + delete require.cache[require.resolve(localePath)]; + res = require(localePath); + } catch (err) { + debug(`Error loading theme locale file: ${locale}`); + needsFallback = true; + } + } else { + needsFallback = true; + } + + if (needsFallback) { + // Fallback to English if it's not the locale we're already trying + if (locale !== 'en') { + try { + const enPath = path.join(themeLocalesPath, 'en.json'); + if (fs.existsSync(enPath)) { + // Delete from require cache to ensure fresh reads for theme files + delete require.cache[require.resolve(enPath)]; + res = require(enPath); + } else { + res = {}; + } + } catch (enErr) { + res = {}; + } + } else { + debug(`Theme en.json file not found`); + res = {}; + } + } + + // Handle the same default export issue as other namespaces + acc[locale] = { + theme: {...res, ...(res.default && typeof res.default === 'object' ? res.default : {})} + }; + return acc; + }, {}); +} + +/** + * @param {string} [lng] + * @param {'ghost'|'portal'|'test'|'signup-form'|'comments'|'search'|'theme'} ns + */ +module.exports = (lng = 'en', ns = 'portal', options = {}) => { + const i18nextInstance = i18next.createInstance(); + const interpolation = { + prefix: '{', + suffix: '}' + }; + if (ns === 'theme') { + interpolation.escapeValue = false; + } + let resources; + if (ns !== 'theme') { + resources = generateResources(SUPPORTED_LOCALES, ns); + } else { + debug(`generateThemeResources: ${lng}, ${options.themePath}`); + resources = generateThemeResources(lng, options.themePath); + } + + i18nextInstance.init({ + lng, + + // allow keys to be phrases having `:`, `.` + nsSeparator: false, + keySeparator: false, + + // if the value is an empty string, return the key + returnEmptyString: false, + + // do not load a fallback + fallbackLng: { + no: ['nb', 'en'], + default: ['en'] + }, + + ns: ns, + defaultNS: ns, + + // separators + interpolation, + + resources + }); + + return i18nextInstance; +}; + +module.exports.SUPPORTED_LOCALES = SUPPORTED_LOCALES; +module.exports.LOCALE_DATA = LOCALE_DATA; +module.exports.generateResources = generateResources; diff --git a/ghost/i18n/lib/locale-data.json b/ghost/i18n/lib/locale-data.json new file mode 100644 index 0000000..41b80d8 --- /dev/null +++ b/ghost/i18n/lib/locale-data.json @@ -0,0 +1,64 @@ +[ + {"code": "af", "label": "Afrikaans"}, + {"code": "ar", "label": "Arabic"}, + {"code": "bg", "label": "Bulgarian"}, + {"code": "bn", "label": "Bengali"}, + {"code": "bs", "label": "Bosnian"}, + {"code": "ca", "label": "Catalan"}, + {"code": "cs", "label": "Czech"}, + {"code": "da", "label": "Danish"}, + {"code": "de", "label": "German"}, + {"code": "de-CH", "label": "Swiss German"}, + {"code": "el", "label": "Greek"}, + {"code": "en", "label": "English"}, + {"code": "eo", "label": "Esperanto"}, + {"code": "es", "label": "Spanish"}, + {"code": "et", "label": "Estonian"}, + {"code": "eu", "label": "Basque"}, + {"code": "fa", "label": "Persian/Farsi"}, + {"code": "fi", "label": "Finnish"}, + {"code": "fr", "label": "French"}, + {"code": "gd", "label": "Gaelic (Scottish)"}, + {"code": "he", "label": "Hebrew"}, + {"code": "hi", "label": "Hindi"}, + {"code": "hr", "label": "Croatian"}, + {"code": "hu", "label": "Hungarian"}, + {"code": "id", "label": "Indonesian"}, + {"code": "is", "label": "Icelandic"}, + {"code": "it", "label": "Italian"}, + {"code": "ja", "label": "Japanese"}, + {"code": "ko", "label": "Korean"}, + {"code": "kz", "label": "Kazakh"}, + {"code": "lt", "label": "Lithuanian"}, + {"code": "lv", "label": "Latvian"}, + {"code": "mk", "label": "Macedonian"}, + {"code": "mn", "label": "Mongolian"}, + {"code": "ms", "label": "Malay"}, + {"code": "nb", "label": "Norwegian Bokmål"}, + {"code": "ne", "label": "Nepali"}, + {"code": "nl", "label": "Dutch"}, + {"code": "nn", "label": "Norwegian Nynorsk"}, + {"code": "pa", "label": "Punjabi"}, + {"code": "pl", "label": "Polish"}, + {"code": "pt", "label": "Portuguese"}, + {"code": "pt-BR", "label": "Portuguese (Brazil)"}, + {"code": "ro", "label": "Romanian"}, + {"code": "ru", "label": "Russian"}, + {"code": "si", "label": "Sinhala"}, + {"code": "sk", "label": "Slovak"}, + {"code": "sl", "label": "Slovenian"}, + {"code": "sq", "label": "Albanian"}, + {"code": "sr", "label": "Serbian"}, + {"code": "sr-Cyrl", "label": "Serbian (Cyrillic)"}, + {"code": "sv", "label": "Swedish"}, + {"code": "sw", "label": "Swahili"}, + {"code": "ta", "label": "Tamil"}, + {"code": "th", "label": "Thai"}, + {"code": "tr", "label": "Turkish"}, + {"code": "uk", "label": "Ukrainian"}, + {"code": "ur", "label": "Urdu"}, + {"code": "uz", "label": "Uzbek"}, + {"code": "vi", "label": "Vietnamese"}, + {"code": "zh", "label": "Chinese"}, + {"code": "zh-Hant", "label": "Traditional Chinese"} +] diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json new file mode 100644 index 0000000..f1cbeb1 --- /dev/null +++ b/ghost/i18n/locales/context.json @@ -0,0 +1,424 @@ +{ + "(save {highestYearlyDiscount}%)": "Appears in portal next to the yearly plan selector", + "+1 (123) 456-7890": "Placeholder for phone number input field", + "1 comment": "Comment count displayed above the comments section in case there is only one", + "1 month": "Duration label for a 1 month subscription", + "1 month free": "Retention offer discount badge shown during subscription cancellation", + "1 year": "Duration label for a 1 year subscription", + "Access your RSS feeds": "Default description text for the Transistor podcast integration in Portal", + "Account": "A label in Portal for your account area", + "Account details updated successfully": "Popover message in Portal", + "Account settings": "A label in Portal for your account settings", + "Add a personal note": "Becomes the field label for donations in Stripe", + "Add comment": "Button text to post a comment", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Invitation to include additional info when commenting", + "Add reply": "Button text to post your reply", + "Add your expertise": "A link in the comments to add ones expertise if not yet provided", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Confirmation message explaining how free trials work during signup", + "All the best!": "A light-hearted ending to an email", + "Already a member?": "A link displayed on signup screen, inviting people to log in if they have already signed up previously", + "An error occurred": "An error message in Portal", + "An unexpected error occured. Please try again or contact support if the error persists.": "Notification if an unexpected error occurs.", + "Anonymous": "Comment placed by a member without a name", + "Are you sure?": "Asks the user to confirm deleting a comment", + "Authors": "Label for search results", + "Back": "A button to return to the previous page", + "Back to Log in": "A button to return to the login screen", + "Become a member of {publication} to start commenting.": "Call to action in the comments app", + "Become a paid member of {publication} to start commenting.": "Call to action in the comments app", + "Become a paid member of {site} to get access to all premium content.": "Call to action for paid content", + "Before you go": "Page heading in Portal shown when presenting a retention offer during subscription cancellation", + "Best": "Text appears in the sorting selector for comments", + "Billing info & receipts": "Title in Portal for the section which links to Stripe's billing portal", + "Black Friday": "An example offer name", + "Bluesky": "Share option in Portal share modal overflow menu", + "By {authors}": "Newsletter header text", + "Cancel": "Button text", + "Cancel anytime.": "A label explaining that the trial can be cancelled at any time", + "Cancel subscription": "A button to cancel a paid subscription", + "Canceled": "Badge label shown on Portal account page when a subscription has been canceled", + "Cancellation reason": "A textarea inviting members who are publishing to share feedback with the publisher", + "Change": "A button to change the current plan", + "Change plan": "Header for the change plan screen in Portal", + "Check spam & promotions folders": "A section title in email receiving FAQ", + "Check with your mail provider": "A section title in email receiving FAQ", + "Check your inbox to verify email update": "Displayed when members change email addresses in Portal", + "Choose": "A button to select a plan", + "Choose a different plan": "A button for members to switch between plans", + "Choose a plan": "Header for the plan selection screen in Portal", + "Choose your newsletters": "A title for a screen where members can choose which email newsletters to receive", + "Click here to retry": "A link to retry the login process", + "Close": "A button to close or dismiss portal and other UI components", + "Code": "Aria-label for the one-time code input field on the verification page in Portal", + "Comment": "Button text to post your comment, on smaller devices", + "Comment preferences updated.": "Shown when a user changes preferences in Portal", + "Commenting disabled": "Heading shown in the comments UI when a member's commenting privileges have been disabled", + "Comments": "A title for the comments section on a post", + "Complete signup for {siteTitle}!": "Magic link email", + "Complete your profile": "Title of the modal to edit your expertise in the comments app", + "Complete your sign up to {siteTitle}!": "Magic link email", + "Complimentary": "Label of a paid plan which has been added to a member's account for free", + "Confirm": "A button to confirm", + "Confirm cancellation": "A button to confirm the cancellation of a subscription", + "Confirm email address": "Title for a modal for confirming. As the modal calls for action, the imperative form shall be used", + "Confirm signup": "A link to confirm the email in signup", + "Confirm subscription": "Button to confirm the change to a new plan", + "Confirm your email address": "Subject of the confirmation email sent to members", + "Confirm your email update for {siteTitle}!": "Sent to a member when they change their email address", + "Confirm your subscription to {siteTitle}": "Magic link email", + "Contact support": "Button to send an email to support", + "Continue": "Continue button", + "Continue subscription": "Button to continue a (cancelled) paid subscription", + "Copied": "Replaces 'Copy link' button label in Portal share modal after URL is copied", + "Copy link": "Button in Portal share modal to copy the post URL to clipboard", + "Could not create Stripe billing portal session": "Error message in Portal's data-attributes handler when the Stripe billing portal fails to open via a theme [data-members-manage-billing] element", + "Could not create Stripe checkout session": "An error message indicating a problem with Stripe", + "Could not sign in. Login link expired.": "Message when a login link has expired", + "Could not update email! Invalid link.": "Message when an email update link is invalid", + "Create a new contact": "A section title in email receiving FAQ", + "Current plan": "Label for the current plan", + "Delete": "Delete button", + "Delete account": "A button for members to delete their account", + "Deleted": "Shown when a member deletes a comment", + "Deleted member": "Name of a member used for comments when the member has been deleted", + "Deleting": "Shown while a comment deletion is in progress", + "Device:": "Label for device verification (forthcoming)", + "Didn't mean to do this? Manage your preferences .": "Message shown after unsubscribing from a newsletter", + "Discussion": "Short default title for the comments section used on smaller devices", + "Don't have an account?": "A link on the login screen, directing people who do not yet have an account to sign up.", + "Edit": "A button to edit the user profile", + "Edit this comment": "Context menu action to edit a comment", + "Email": "A label for email address input", + "Email newsletter": "Title for the email newsletter settings", + "Email newsletter settings updated": "A message showing newsletter settings have been updated", + "Email preferences": "A label for email settings", + "Email preferences updated.": "Shown when a member updates email preferences", + "Email sent": "Button text after being clicked, when an email has been sent to confirm a new subscription. Should be short.", + "Emails": "A label for a list of emails", + "Emails disabled": "Title for a message in portal telling members that they are not receiving emails, due to repeated delivery failures to their address", + "Ends {offerEndDate}": "Portal - label for an offer that ends on a specific date", + "Enjoy a free month on us.": "Retention offer message in Portal shown when offering one free month to a member cancelling their subscription", + "Enjoy a free month on us. You won't be charged until {newBillingDate}.": "Retention offer message in Portal shown when offering one free month, with the next billing date", + "Enjoy {amountOff} off forever.": "Retention offer message in Portal for a permanent discount offer during subscription cancellation", + "Enjoy {months} free months on us.": "Retention offer message in Portal for multi-month free offers during subscription cancellation", + "Enjoy {months} free months on us. You won't be charged until {newBillingDate}.": "Retention offer message in Portal for multi-month free offers, with the next billing date", + "Enter code above": "Portal - error message when OTC code isn't filled in", + "Enter your email address": "Error message when an email address is missing", + "Enter your name": "Placeholder input when editing your name in the comments app in the expertise dialog", + "Error": "Status indicator for a notification", + "Expertise": "Input label when editing your expertise in the comments app", + "Expires {expiryDate}": "Label for when a complimentary subscription expires (in portal)", + "Facebook": "Share option in Portal share modal overflow menu", + "Failed to cancel subscription, please try again": "error message", + "Failed to log in, please try again": "error message", + "Failed to log out, please try again": "error message", + "Failed to open billing portal, please try again": "Error notification in Portal when the Stripe billing portal cannot be opened", + "Failed to process checkout, please try again": "error message", + "Failed to send magic link email": "error message", + "Failed to send verification email": "error message", + "Failed to sign up, please try again": "error message", + "Failed to update account data": "error message", + "Failed to update account details": "error message", + "Failed to update billing information, please try again": "Error notification in Portal when updating payment method or billing address fails", + "Failed to update newsletter settings": "error message", + "Failed to update subscription, please try again": "error message", + "Failed to verify code, please try again": "Error message when one-time code verification fails during sign-in", + "For security verification, enter the code below to sign in to {siteTitle}:": "Device verification email", + "For your security, the link will expire in 24 hours time.": "Descriptive text in emails about authentication links", + "Forever": "Label for a discounted price which has no expiry date", + "Founder @ Acme Inc": "Placeholder value for the input box when editing your expertise", + "Free Trial – Ends {trialEnd}": "Portal - label for a free trial that ends on a specific date", + "Full-time parent": "Example of an expertise of a person used in comments when editing your expertise", + "Get help": "A link to contact support", + "Get in touch for help": "A section title in email receiving FAQ", + "Get notified when someone replies to your comment": "A label for a setting allowing email notifications to be received", + "Gift subscription": "Subscription label for members who have an active gift subscription", + "Give feedback on this post": "A label that goes with the member feedback buttons at the bottom of newsletters", + "Head of Marketing at Acme, Inc": "Example of an expertise of a person used in comments when editing your expertise", + "Help! I'm not receiving emails": "A section title in email receiving FAQ", + "Here are a few other sites you may enjoy.": "Text introducing related sites someone may find interesting", + "Here's your code to login to {siteTitle}": "Device verification email", + "Hey there!": "An introduction/opening to an email", + "Hey there,": "An introduction/opening to an email", + "Hidden for members": "Shown to moderators when a comment is hidden from members", + "Hide": "Action in the context menu for administrators, on smaller devices", + "Hide comment": "Action in the context menu for administrators", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Paragraph in the email suppression FAQ", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Paragraph in the email suppression FAQ", + "If you cancel your subscription now, you will continue to have access until {periodEnd}.": "Portal - shown to a member who is cancelling their subscription.", + "If you did not make this request, you can safely ignore this email.": "Footer text in signup/login emails", + "If you did not make this request, you can simply delete this message.": "Footer text in signup/login emails", + "If you didn't try to sign in recently, you can safely ignore this email to deny access.": "Footer text in signup/login emails", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {senderEmail}": "Footer text in signup/login emails", + "If you have an account, a login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "A message displayed during signin process.", + "If you have an account, an email has been sent to {submittedEmailOrInbox}. Click the link inside or enter your code below.": "A message displayed after requesting a magic link or OTC code.", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Paragraph in the email suppression FAQ", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Paragraph in the email receiving FAQ", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {supportAddress}.": "Email receiving FAQ", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Paragraph in the email suppression FAQ", + "In your email client add {senderEmail} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Email receiving FAQ", + "Invalid email address": "Error message when a provided email address is invalid", + "Invalid verification code": "Error message in Portal when the entered one-time sign-in code is incorrect or expired", + "Jamie Larson": "An unisex name of a person we use in examples", + "Join the discussion": "Placeholder value of the comments input box", + "Just now": "Time indication when a comment has been posted 'just now'", + "Keep reading": "Header above the selection of recent posts, shown in the newsletter email", + "Less like this": "A label for the thumbs-down response in member feedback at the bottom of emails", + "LinkedIn": "Share button in Portal share modal", + "Load more ({amount})": "Button text to load more replies in the comments app", + "Local resident": "Example of an expertise of a person used in comments when editing your expertise", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Paragraph in the email receiving FAQ", + "Manage": "A button for managing settings", + "Manage subscription": "Header in Portal for the subscription management screen", + "Manage your preferences": "Link text in the member welcome email footer for managing communication preferences", + "Maybe later": "An informal phrase to dismiss or close a popup", + "Member discussion": "Default title for the comments section on a post", + "Member since": "Shown in the 'subscription status' section of the newsletter, will be followed by a date", + "Memberships from this email domain are currently restricted.": "Shown when someone tries to sign up with a blocked domain", + "Memberships unavailable, contact the owner for access.": "Inform a user that memberships are only available by contacting the site owner", + "Monthly": "A label to indicate a monthly payment cadence", + "More like this": "A label for the thumbs-up response in member feedback at the bottom of emails", + "More options": "Button in Portal share modal that opens a menu with additional share options", + "Name": "A label to indicate a member's name", + "Need more help? Contact support": "A link to contact support", + "Neurosurgeon": "Example of an expertise of a person used in comments when editing your expertise", + "New comment on {postTitle}": "Title in email notifying author about new comment to hist post", + "New reply to your comment on {siteTitle}": "Email subject when notifying user about reply to his comment", + "Newest": "Text appears in the sorting selector for comments", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "A paragraph in the email suppression FAQ", + "No matches found": "Shown in search when 0 results", + "No member exists with this e-mail address.": "Shown when trying to sign in.", + "No thanks, I want to cancel": "Button in Portal to decline a retention offer and proceed with subscription cancellation", + "Not receiving emails?": "A link in portal to take members to an FAQ area about what to do if you're not receiving emails", + "Now check your email!": "A confirmation message after logging in or signing up", + "Oldest": "Text appears in the sorting selector for comments", + "Once deleted, this comment can’t be recovered.": "Warning message before deleting a comment", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "A paragraph in the email suppression FAQ", + "One hour ago": "Time a comment was placed", + "One min ago": "Time since a comment was made", + "Open AOL Mail": "Shown on signup and signin if your email is detected as AOL Mail", + "Open Feedbin": "Shown on signup and signin if your email is detected as Feedbin", + "Open Gmail": "Shown on signup and signin if your email is detected as Gmail", + "Open Hey": "Shown on signup and signin if your email is detected as Hey", + "Open Mail.ru": "Shown on signup and signin if your email is detected as Mail.ru", + "Open Outlook": "Shown on signup and signin if your email is detected as Outlook", + "Open Proton Mail": "Shown on signup and signin if your email is detected as Proton Mail", + "Open Yahoo Mail": "Shown on signup and signin if your email is detected as Yahoo Mail", + "Open email": "Shown on signup and signin if your email is detected, but we don't know which provider", + "Open iCloud Mail": "Shown on signup and signin if your email is detected as iCloud Mail", + "Or use this link to securely sign in": "Text in the sign-in email above the magic link fallback, shown when a one-time code is provided", + "Or, skip the code and sign in directly": "Text in the sign-in email offering a direct magic link as an alternative to entering the one-time code", + "Permanent failure (bounce)": "A section title in the email suppression FAQ", + "Phone number": "Label for phone number input", + "Plan": "Label for the default subscription plan", + "Plan checkout was cancelled.": "Notification for when a plan checkout was cancelled", + "Plan upgrade was cancelled.": "Notification for when a plan upgrade was cancelled", + "Please confirm your email address with this link:": "Descriptive text in signup emails, right before the button members click to confirm their address", + "Please contact {supportAddress} to adjust your complimentary subscription.": "Directions in portal", + "Please enter a valid email address": "Err message when an email address is invalid", + "Please enter {fieldName}": "error message when a required field is missing", + "Please fill in required fields": "Error message when a required field is missing", + "Podcasts": "Default heading for the Transistor podcast integration section in Portal", + "Posts": "Search result header", + "Price": "A label to indicate price of a tier", + "Re-enable emails": "A button for members to turn-back-on emails, if they have been previously disabled as a result of delivery failures", + "Recommendations": "A suggestion by/for another site of interest to users", + "Renews at {price}.": "Portal - label for a subscription that renews at a specific price", + "Replied to": "Comments label", + "Reply": "Button to reply to a comment", + "Reply to": "Shows what comment is being replied to. (Followed by text of the comment being replied to)", + "Reply to comment": "Placeholder value of the input box when placing a reply to a comment", + "Report": "Button to report a comment", + "Report comment": "Used in the context menu", + "Report this comment?": "Title of the modal to report a comment", + "Resume subscription": "Button in the canceled subscription banner on the Portal account page to resume a canceled subscription", + "Retry": "When something goes wrong, this link allows people to re-attempt the same action", + "Save": "A button to save", + "Save {amountOff} on your next billing cycle. Then {currency}{originalPrice}/{cadence}.": "Retention offer message in Portal for a single-cycle discount during subscription cancellation", + "Save {amountOff} on your next {durationInMonths} billing cycles. Then {currency}{originalPrice}/{cadence}.": "Retention offer message in Portal for a multi-cycle discount during subscription cancellation", + "Search posts, tags and authors": "Search placeholder text", + "Secure sign in link for {siteTitle}": "Magic link email", + "See you soon!": "A sign-off/ending to a signup email", + "Send an email and say hi!": "A section title in email receiving FAQ", + "Send an email to {senderEmail} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Email receiving FAQ", + "Sending": "Button text when reporting a comment", + "Sending login link...": "A loading status message when a member has just clicked to login", + "Sending...": "A loading status message when an email is being sent", + "Sent": "Button text when a comment was reported succesfully", + "Sent to {email}": "Magic link email footer", + "Share": "Title of Portal share modal for sharing a post, and label for the newsletter email footer Share button text passed as buttonText to the feedbackButton partial", + "Show": "Context menu action for a comment, for administrators - smaller devices", + "Show 1 more reply": "Button text to load the last remaining reply for a comment", + "Show all": "Show all recommendations", + "Show comment": "Context menu action for a comment, for administrators", + "Show more results": "Link shown if there are additional search results available", + "Show {amount} more replies": "Button text to load more replies in the comments app", + "Sign in": "A button to sign in", + "Sign in now": "Button text in the sign-in email when a one-time code is provided", + "Sign in to {siteTitle}": "Magic link email", + "Sign in to {siteTitle} with code {otc}": "Subject line for the sign-in email when a one-time code is sent", + "Sign in verification": "Magic link email", + "Sign out": "A button to sign out", + "Sign up": "A button to sign up", + "Sign up now": "Button text to sign up in order to post a comment", + "Signup error: Invalid link": "Notification text when an invalid / expired signup link is used", + "Signups from this email domain are currently restricted.": "error message", + "Someone just replied to your comment": "Email text when informing user about reply to his comment", + "Someone just replied to your comment on {postTitle}.": "Email text when informing user about reply to his comment on post", + "Something went wrong, please try again later.": "error message", + "Something went wrong, please try again.": "Error message when subscribing to a newsletter fails in the signup form embed", + "Sorry, no recommendations are available right now.": "Shown if a user visits the recommendations screen in portal when recommendations aren't available", + "Sorry, that didn’t work.": "Title of a page when an error occured while submitting feedback", + "Sort by": "Used for sorting comments, appears next to a dropdown with options (oldest, newest, best)", + "Spam complaints": "A title in the email suppression FAQ", + "Start the conversation": "Title of the CTA section of comments when not signed in and when there are no comments yet", + "Start {amount}-day free trial": "Portal - label for a free trial that lasts a specific number of days", + "Starting today": "Message when a subscription starts today", + "Starting {startDate}": "Portal - label for a subscription that starts on a specific date (after trial)", + "Submit feedback": "A button for submitting member feedback", + "Subscribe": "Title of a section for subscribing to a newsletter", + "Subscribed": "Status of a newsletter which a member has subscribed to", + "Subscription details": "Label at the top of the newsletter section", + "Subscription plan updated successfully": "Popover message in Portal", + "Success": "Status indicator for a notification", + "Success! Check your email for magic link to sign-in.": "Notification text when the user has been sent a magic link to sign-in", + "Success! Your account is fully activated, you now have access to all content.": "Notification text when a user has activated their email and can access content", + "Success! Your email is updated.": "Notification text when a user has updated their email address", + "Successfully unsubscribed": "A confirmation message when a member clicks an unsubscribe link", + "Tags": "Search results header", + "Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:": "Magic link email", + "Thank you for signing up to {siteTitle}!": "Magic link email", + "Thank you for subscribing to {siteTitle}!": "Magic link email", + "Thank you for subscribing to {siteTitle}.": "Magic link email", + "Thank you for subscribing to {siteTitle}. Tap the link below to be automatically signed in:": "Magic link email", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Subscription cofirmation message with notice of recommended sites", + "Thank you for your support": "A success message, shown in a modal after a non-member has made a tip or donation", + "Thank you for your support!": "A success message, shown in a notification after a member has made a tip or donation", + "Thanks for the feedback!": "A confirmation message after submitting member feedback", + "That didn't go to plan": "An error message indicating something went wrong", + "The email address we have for you is {memberEmail} — if that's not correct, you can update it in your .": "Email receiving FAQ", + "The linked comment is no longer available.": "A notice shown when a permalink targets a comment that has been hidden or deleted", + "There was a problem submitting your feedback. Please try again a little later.": "An error message for when submitting feedback has failed", + "There was an error cancelling your subscription, please try again.": "error message", + "There was an error continuing your subscription, please try again.": "error message", + "There was an error processing your payment. Please try again.": "error message", + "There was an error sending the email, please try again": "error message", + "This comment has been hidden.": "Text for a comment thas was hidden", + "This comment has been removed.": "Text for a comment thas was removed", + "This email address will not be used.": "This is in the footer of signup verification emails, and comes right after 'If you did not make this request, you can simply delete this message.'", + "This message was sent from {siteDomain} to {email}.": "Text in the footer of emails, e.g. reply to comments notifications", + "This site is invite-only, contact the owner for access.": "A message on the member login screen indicating that a site is not-open to public signups", + "This site is not accepting donations at the moment.": "Error message in Portal when a user visits the donations page but donations are disabled", + "This site is not accepting payments at the moment.": "An error message shown when a tips or donations link is opened but the site has donations disabled", + "This site only accepts paid members.": "error message for sign-up", + "Threads": "Share option in Portal share modal overflow menu", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "A confirmation message displayed during the signup process, indicating that the person signing up needs to go and check their email - and reminding them to check their spam folder, too", + "To continue to stay up to date, subscribe to {publication} below.": "In the portal support section", + "Too many attempts try again in {number} days.": "Error message when a user has tried to sign in too many times", + "Too many attempts try again in {number} hours.": "Error message when a user has tried to sign in too many times", + "Too many attempts try again in {number} minutes.": "Error message when a user has tried to sign in too many times", + "Too many different sign-in attempts, try again in {number} days": "Error message when a user has tried to sign in too many times", + "Too many different sign-in attempts, try again in {number} hours": "Error message when a user has tried to sign in too many times", + "Too many different sign-in attempts, try again in {number} minutes": "Error message when a user has tried to sign in too many times", + "Too many sign-up attempts, try again later": "error message", + "Try free for {amount} days, then {originalPrice}.": "Portal - label for a free trial that lasts a specific number of days, followed by the original price", + "Unable to initiate checkout session": "error message", + "Unlock access to all newsletters by becoming a paid subscriber.": "A message to encourage members to upgrade to a paid subscription", + "Unsubscribe": "Link in the newsletter", + "Unsubscribe from all emails": "A button on the unsubscribe page, offering a shortcut to unsubscribe from every newsletter at the same time", + "Unsubscribe from comment reply notifications": "Link text in comments reply email to unsubscribe from notifications", + "Unsubscribed": "Status of a newsletter which a user has not subscribed to", + "Unsubscribed from all emails.": "Shown if a member unsubscribes from all emails", + "Unsubscribing from emails will not cancel your paid subscription to {title}": "Portal - newsletter subscription management", + "Update": "A button to update the billing information", + "Update your preferences": "A button for updating member preferences in their account area", + "Upgrade": "An action button in the newsletter call-to-action", + "Upgrade now": "Button text in the comments section, when you need to be a paid member in order to post a comment", + "Upgrade to continue reading.": "Appears in the call to action", + "Verification link sent, check your inbox": "Instruction to check email for recommendation verification", + "Verify your email address is correct": "A section title in the email receiving FAQ", + "Verifying...": "Button text in Portal shown while a one-time sign-in code is being verified", + "View comments": "Link text in comment email to view the comments", + "View in admin": "Context menu item in the comments UI that opens a comment in the Ghost admin panel", + "View in browser": "Link in the newsletter header taking the user to the website", + "View plans": "A button to view available plans", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "An error message when an unsubscribe link is clicked, but we don't have any record of the address being unsubscribed. Probably because the email address on the account has been changed.", + "We'd hate to see you leave. How about a special offer to stay?": "Default description in Portal retention offer section during subscription cancellation", + "Welcome back to {siteTitle}!": "Popup when a member logs in", + "Welcome back to {siteTitle}! Your verification code is {otc}.": "Email preheader text for the sign-in email when a one-time code is sent", + "Welcome back!": "A message when a user has logged in successfully", + "Welcome back! Here's your code to sign in to {siteTitle}": "Paragraph in the sign-in email introducing the one-time code", + "Welcome back! Use this link to securely sign in to your {siteTitle} account:": "Magic link email", + "Welcome back, {name}!": "Popup when a member logs in", + "Welcome to {siteTitle}": "Shown in recommendations section of Portal", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "A paragraph from the email suppression FAQ", + "When:": "Device verification (forthcoming)", + "Where:": "Device verification (forthcoming)", + "Why has my email been disabled?": "A section title from the email suppression FAQ", + "X (Twitter)": "Share button in Portal share modal", + "Yearly": "A label indicating an annual payment cadence", + "Yesterday": "Time a comment was placed", + "You are receiving this because you are a %%{status}%% subscriber to {site}.": "Shown at the bottom of the newsletter. Status will be one of free/paid/trialing/complimentary - see those strings below.", + "You can also copy & paste this URL into your browser:": "Descriptive text displayed underneath the buttons in login/signup emails, right before authentication URLs which can be copy/pasted", + "You can unsubscribe from these notifications at {profileUrl}.": "Footer text in comments email to manage notification preferences for users", + "You can't post comments in this publication.": "Shown in the comments UI when a member's commenting privileges have been disabled and no support email is configured", + "You can't post comments in this publication. Contact support for more information.": "Shown in the comments UI when a member's commenting privileges have been disabled. The ... tag wraps a mailto link to the publication's support email address", + "You currently have a free membership, upgrade to a paid subscription for full access.": "A message indicating that the member is using a free subcription, and could access more content with a paid subscription", + "You have been successfully resubscribed": "A confirmation message when a member has had emails turned off, but they have been successfully turned back on", + "You just tried to access your account from a new device.": "Device verification (forthcoming)", + "You will not be signed up, and no account will be created for you.": "Descriptive text in signup emails indicating that if someone does NOT click on the confirmation button, then they will not be signed up to anything. This text is intended to reassure people who receive a signup confirmation email that they did not ask for.", + "You will not be subscribed.": "Descriptive text in signup emails indicating that if someone does NOT click on the confirmation button, then they will not be signed up to anything. This text is intended to reassure people who receive a signup confirmation email that they did not ask for.", + "You're currently not receiving emails": "Message for user who have newsletters disabled", + "You're not receiving emails": "Shorter message for user who have newsletters disabled, for mobile devices", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "An error message displayed in the member account area when newsletter delivery to their address has repeatedly failed or they have marked an email as spam.", + "You're one tap away from subscribing to {siteTitle} — please confirm your email address with this link:": "Signup confirmation email", + "You're one tap away from subscribing to {siteTitle}!": "Signup confirmation email", + "You've successfully signed in.": "A notification displayed when the user signs in", + "You've successfully subscribed to {siteTitle}": "Notification displayed after a successful signup. The ... tag wraps the site name. {siteTitle} is the name of the publication", + "Your account": "A label indicating member account details", + "Your email": "Form label for the email address input in e.g. signup forms", + "Your email address": "Placeholder text in an input field", + "Your email has failed to resubscribe, please try again": "error message in portal", + "Your free trial ends on {date}, at which time you will be charged the regular price. You can always cancel before then.": "Newsletter text - shown to trialing members when receiving the newsletter (if subscription status is being shown)", + "Your input helps shape what gets published.": "Descriptive text displayed on the member feedback UI, telling people how their feedback is used", + "Your name": "Form label for the name input in e.g. signup forms", + "Your request will be sent to the owner of this site.": "Descriptive text displayed in the report comment modal", + "Your subscription has been canceled and will expire on {date}. You can resume your subscription via your account settings.": "Shown in the newsletter footer when a member has cancelled their subscription but it hasn't expired yet.", + "Your subscription has been canceled and will expire on {expiryDate}.": "Banner message shown on Portal account page when a subscription is canceled but still active until the period end", + "Your subscription has expired.": "Subscription status, included at the bottom of newsletters", + "Your subscription will expire on {date}.": "Appears in the newsletter footer, be sure to include {date} in the translation.", + "Your subscription will expire on {expiryDate}": "Portal account page", + "Your subscription will renew on {date}.": "Appears in the newsletter footer, be sure to include {date} in the translation.", + "Your subscription will renew on {renewalDate}": "Portal account page", + "Your subscription will start on {subscriptionStart}": "Portal account page", + "Your verification code for {siteTitle}": "Device verification email", + "complimentary": "Will be substituted into status for 'You are receiving this because you are a %%{status}%% subscriber to {site}.'", + "edited": "label for an edited comment", + "free": "Will be substituted into status for 'You are receiving this because you are a %%{status}%% subscriber to {site}.'", + "jamie@example.com": "Placeholder for email input field", + "month": "the subscription interval (monthly), following the /", + "paid": "Will be substituted into status for 'You are receiving this because you are a %%{status}%% subscriber to {site}.'", + "removed": "label for removed comment", + "trialing": "Will be substituted into status for 'You are receiving this because you are a %%{status}%% subscriber to {site}.'", + "year": "the subscription interval (monthly), following the /", + "your inbox": "Fallback text substituted into 'submittedEmailOrInbox' in Portal sign-in confirmation when no email address is available", + "{amount} characters left": "shown in comments editor", + "{amount} comments": "shown in the comments app", + "{amount} days free": "Portal - label for a free trial that lasts a specific number of days", + "{amount} hrs ago": "Time since this comment was placed", + "{amount} mins ago": "Time since this comment was placed", + "{amount} more": "Button text to load more replies in the comments app", + "{amount} off": "Portal offer page, showing a discount", + "{amount} off for first {number} months.": "Portal offer page, showing a discount", + "{amount} off for first {period}.": "Portal offer page, showing a discount", + "{amount} off forever.": "Portal offer page, showing a discount", + "{date}": "This will appear at the top of the newsletter, as the publication date. No changes needed, usually.", + "{discount}% discount": "Label for a discounted tier in portal", + "{memberEmail} will no longer receive emails when someone replies to your comments.": "Shown when a member unsubscribes from comment replies", + "{memberEmail} will no longer receive this newsletter.": "Shown when a member unsubscribes from a newsletter", + "{memberEmail} will no longer receive {newsletterName} newsletter.": "Shown when a member unsubscribes from a newsletter", + "{months} months": "Duration label for a multiple-month subscription", + "{months} months free": "Retention offer discount badge shown during subscription cancellation", + "{trialDays} days free": "Portal - label for a free trial that lasts a specific number of days", + "{years} years": "Duration label for a multiple-year subscription" +} \ No newline at end of file diff --git a/ghost/i18n/package.json b/ghost/i18n/package.json new file mode 100644 index 0000000..9af6140 --- /dev/null +++ b/ghost/i18n/package.json @@ -0,0 +1,43 @@ +{ + "name": "@tryghost/i18n", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/ghost/i18n", + "author": "Ghost Foundation", + "private": true, + "main": "index.js", + "exports": { + ".": "./index.js", + "./lib/locale-data.json": "./lib/locale-data.json" + }, + "types": "./build/i18n.d.ts", + "scripts": { + "dev": "echo \"Implement me!\"", + "test:base": "NODE_ENV=testing c8 --include index.js --include lib --check-coverage --100 --reporter text --reporter cobertura -- mocha --reporter dot './test/**/*.test.js'", + "test": "pnpm test:base && pnpm translate", + "lint:code": "eslint *.js lib/ --ext .js --cache", + "lint": "pnpm lint:code && pnpm lint:test && pnpm lint:translations", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", + "lint:translations": "node ./test/i18n.lint.js", + "translate": "pnpm translate:ghost && pnpm translate:portal && pnpm translate:signup-form && pnpm translate:comments && pnpm translate:search && node generate-context.js", + "translate:ghost": "NAMESPACE=ghost i18next '../core/core/{frontend,server,shared}/**/*.{js,jsx}' '../core/core/server/services/email-rendering/partials/**/*.hbs' '../core/core/server/services/email-service/email-templates/**/*.hbs' '../core/core/server/services/comments/email-templates/**/*.hbs' '../core/core/server/services/member-welcome-emails/email-templates/**/*.hbs'", + "translate:portal": "NAMESPACE=portal i18next '../../apps/portal/src/**/*.{js,jsx}'", + "translate:signup-form": "NAMESPACE=signup-form i18next '../../apps/signup-form/src/**/*.{ts,tsx}'", + "translate:comments": "NAMESPACE=comments i18next '../../apps/comments-ui/src/**/*.{ts,tsx}'", + "translate:search": "NAMESPACE=search i18next '../../apps/sodo-search/src/**/*.{js,jsx,ts,tsx}'" + }, + "files": [ + "index.js", + "lib", + "locales" + ], + "devDependencies": { + "c8": "10.1.3", + "glob": "^13.0.6", + "i18next-parser": "8.13.0", + "mocha": "11.7.5" + }, + "dependencies": { + "@tryghost/debug": "0.1.40", + "i18next": "23.16.8" + } +} diff --git a/ghost/i18n/test/.eslintrc.js b/ghost/i18n/test/.eslintrc.js new file mode 100644 index 0000000..1c68044 --- /dev/null +++ b/ghost/i18n/test/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ], + rules: { + // Enforce kebab-case (lowercase with hyphens) for all filenames + 'ghost/filenames/match-regex': ['error', '^[a-z0-9.-]+$', false] + } +}; diff --git a/ghost/i18n/test/i18n-ignores.json b/ghost/i18n/test/i18n-ignores.json new file mode 100644 index 0000000..422d780 --- /dev/null +++ b/ghost/i18n/test/i18n-ignores.json @@ -0,0 +1,143 @@ +{ + "overrides": { + "ghost/i18n/no-undefined-variables": [ + { + "file": "da/portal.json", + "key": "Try free for {amount} days, then {originalPrice}.", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "de-CH/portal.json", + "key": "Memberships unavailable, contact the owner for access.", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "de-CH/portal.json", + "key": "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "is/portal.json", + "key": "Your subscription will renew on {renewalDate}", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "lv/portal.json", + "key": "There was a problem submitting your feedback. Please try again a little later.", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "si/portal.json", + "key": "Your subscription will renew on {renewalDate}", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "si/portal.json", + "key": "Your subscription will start on {subscriptionStart}", + "comment": "FIXME: This translation doesn't work and needs to be updated" + }, + { + "file": "th/portal.json", + "key": "Try free for {amount} days, then {originalPrice}.", + "comment": "FIXME: This translation doesn't work and needs to be updated" + } + ], + "ghost/i18n/no-unused-variables": [ + { + "file": "da/portal.json", + "key": "Try free for {amount} days, then {originalPrice}.", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "de-CH/ghost.json", + "key": "Confirm your email update for {siteTitle}!", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "de-CH/ghost.json", + "key": "Confirm your subscription to {siteTitle}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "de-CH/portal.json", + "key": "Unsubscribing from emails will not cancel your paid subscription to {title}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "de/portal.json", + "key": "Start {amount}-day free trial", + "comment": "translated text doesn't fit on the button" + }, + { + "file": "is/portal.json", + "key": "{trialDays} days free", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "is/portal.json", + "key": "Your subscription will renew on {renewalDate}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "ja/ghost.json", + "key": "Confirm your email update for {siteTitle}!", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "ja/ghost.json", + "key": "Tap the link below to complete the signup process for {siteTitle}, and be automatically signed in:", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "lt/portal.json", + "key": "Renews at {price}.", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "nn/portal.json", + "key": "{amount} off", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "nn/portal.json", + "key": "{discount}% discount", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "pt/comments.json", + "key": "{amount} more", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "si/portal.json", + "key": "Your subscription will renew on {renewalDate}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "si/portal.json", + "key": "Your subscription will start on {subscriptionStart}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "sk/ghost.json", + "key": "By {authors}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "sq/ghost.json", + "key": "Sign in to {siteTitle}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "sq/portal.json", + "key": "Expires {expiryDate}", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + }, + { + "file": "th/portal.json", + "key": "Try free for {amount} days, then {originalPrice}.", + "comment": "FIXME: This error was automatically ignored. Please update the translation, or explain why a variable is ignored" + } + ] + } +} \ No newline at end of file diff --git a/ghost/i18n/test/i18n.lint.js b/ghost/i18n/test/i18n.lint.js new file mode 100644 index 0000000..7497668 --- /dev/null +++ b/ghost/i18n/test/i18n.lint.js @@ -0,0 +1,489 @@ +// @ts-check +const assert = require('node:assert/strict'); +const fs = require('fs/promises'); +const path = require('path'); +const {ESLint} = require('eslint'); +const {glob} = require('glob'); +const {readFileSync, writeFileSync} = require('fs'); + +const LOCALES_ROOT = path.join(__dirname, '..', 'locales'); + +/** + * @typedef {{ + results: import('eslint').ESLint.LintResult[]; + errorCount: number; + ignoreAllFlagProvidedWithoutFixFlag: boolean; + }} LintSummary + + * @typedef { + 'ghost/i18n/no-unused-variables' | + 'ghost/i18n/no-undefined-variables' | + 'ghost/i18n/no-invalid-translations' | + 'ghost/i18n/no-unused-ignores' + } GhostI18nLintRule + */ + +class File { + /** + * @param {string} filePath + * @param {string} source + */ + constructor(filePath, source) { + this.path = filePath; + this.relativePath = path.relative(LOCALES_ROOT, filePath); + this.source = source; + this.parsed = JSON.parse(source); + this.locale = this._extractLocale(filePath); + /** @private */ + this._analysis = null; + } + + /** + * Returns the probabilistic position of the given text in the file. + * @param {string} text + * @param {'key' | 'value'} textType + * @returns {{line: number, column: number}} + */ + getPosition(text, textType) { + return this._analyzedSource()[textType][text] ?? {line: 0, column: 0}; + } + + /** + * Returns a probabilistic location of keys and values in the file + * Assumes the file contains one key/value pair per line + * @private + */ + _analyzedSource() { + if (this._analysis) { + return this._analysis; + } + + const keys = {}; + const values = {}; + + const lines = this.source.split('\n'); + const kvSeparator = /"\s*:\s*"/; + lines.shift(); + lines.pop(); + + // Since we shifted the array, start at 1 + let index = 1; + for (const line of lines) { + index += 1; + const [keyMatch, valueMatch] = line.split(kvSeparator); + if (!keyMatch || !valueMatch) { + continue; + } + + const key = keyMatch.split('"')[1]; + const value = valueMatch.split('"')[0]; + keys[key] = {line: index, column: line.indexOf(key)}; + values[value] = {line: index, column: line.indexOf(value, keyMatch.length + 1)}; + } + + this._analysis = {key: keys, value: values}; + return this._analysis; + } + + /** + * Extracts the locale from the file path. Assumes the file structure is $LOCALE_ROOT/$LOCAL/$NAMESPACE.json + * @param {string} filePath + */ + _extractLocale(filePath) { + const directory = path.dirname(filePath); + return directory.slice(directory.lastIndexOf('/') + 1); + } +} + +/** + * Reads and transforms an ignore file into a Map. + * Keys follow the format of `rule:locale/namespace:key`. + * The value is the index which can be used for updating the ignore file. + * @param {File} ignoreFile + */ +function parseIgnores(ignoreFile) { + const response = new Map(); + ignoreFile.parsed.overrides ??= {}; + const ignores = ignoreFile.parsed.overrides; + for (const [rule, ignored] of Object.entries(ignores)) { + assert(Array.isArray(ignored), `Expected .overrides.${rule} to be an Array`); + for (const [index, ignore] of ignored.entries()) { + assert(typeof ignore === 'object', `Expected .overrides.${rule}[${index}] to be an Object`); + assert(typeof ignore.file === 'string', `Expected .overrides.${rule}[${index}].file to be a String`); + assert(typeof ignore.key === 'string', `Expected .overrides.${rule}[${index}].key to be a String`); + assert(typeof ignore.comment === 'string', `Expected .overrides.${rule}[${index}].comment to be a String`); + assert(ignore.comment, `Expected .overrides.${rule}[${index}].comment to contain a comment`); + + response.set(`${rule}:${ignore.file}:${ignore.key}`, index); + } + } + + return response; +} + +class LinterContext { + /** + * @param {string} ignoreFile + */ + constructor(ignoreFile) { + this.ignoreAll = process.argv.includes('--unsafe-ignore-all'); + this.applyFixes = process.argv.includes('--fix'); + + /** @type {LintSummary} */ + this.summary = { + results: [], + errorCount: 0, + ignoreAllFlagProvidedWithoutFixFlag: this.ignoreAll && !this.applyFixes + }; + + /** @type {File | null} */ + this.file = null; + + this._cwd = process.cwd(); + + /** @type {import('eslint').ESLint.LintResult | null} */ + this._currentResult = null; + this._currentResultIndex = -1; + this._currentMessageIndex = 0; + + this._ignoreFile = new File(ignoreFile, readFileSync(ignoreFile, 'utf8')); + this._ignoredRules = parseIgnores(this._ignoreFile); + this._unusedIgnores = new Map(this._ignoredRules); + + /** + * @type {{resultIndex: number; messageIndex: number; fix: (overrides: any) => void;}[]} + */ + this._fixes = []; + } + + /** + * @param {string} message + * @param {string} key + * @param {GhostI18nLintRule} ruleName + * @param {number} line + * @param {number} column + * @param {(ignores: any) => void} [fix] + */ + reportTranslationError(message, key, ruleName, line, column, fix = undefined) { + const ignoreKey = `${ruleName}:${this.relativeFilePath}:${key}`; + this._unusedIgnores.delete(ignoreKey); + if (!this._ignoredRules.has(ignoreKey)) { + this._reportError(message, ruleName, line, column, fix); + } + } + + /** + * @param {string} text + * @param {'key' | 'value'} textType + */ + getPositionForText(text, textType) { + assert(this.file, 'setFile() should have been called'); + return this.file.getPosition(text, textType); + } + + /** + * @param {File} file + */ + setFile(file) { + this.file = file; + this._currentMessageIndex = 0; + this._currentResultIndex += 1; + this.summary.errorCount += this._currentResult?.errorCount ?? 0; + this._currentResult = { + filePath: path.relative(this._cwd, file.path), + messages: [], + + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + fatalErrorCount: 0, + suppressedMessages: [] + }; + this.summary.results.push(this._currentResult); + } + + get relativeFilePath() { + assert(this.file, 'setFile() should have been called'); + return this.file.relativePath; + } + + complete() { + assert(this._currentResult, 'setFile() should have been called'); + this.summary.errorCount += this._currentResult.errorCount; + + this._reportUnusedIgnores(); + + if (this.applyFixes) { + this._applyFixes(); + } + } + + /** + * @private + * @param {string} message + * @param {GhostI18nLintRule} ruleName + * @param {number} line + * @param {number} column + * @param {(ignores: any) => void} [fix] + */ + _reportError(message, ruleName, line, column, fix = undefined) { + assert(this._currentResult, '`setFile` should have been called'); + this._currentResult.errorCount += 1; + this._currentMessageIndex = this._currentResult.messages.length; + this._currentResult.messages.push({ + ruleId: ruleName, + severity: 2, // Error + message, + line, + column + }); + + if (fix) { + this._fixes.push({fix, resultIndex: this._currentResultIndex, messageIndex: this._currentMessageIndex}); + } + } + + /** @private */ + _reportUnusedIgnores() { + this.setFile(this._ignoreFile); + for (const [key, index] of this._unusedIgnores.entries()) { + const [rule, file, translation] = key.split(':'); + this._reportError( + `Index ${index} for rule "${rule}" is not used.\n\tFile: ./locales/${file}\n\tkey: "${translation}"`, + 'ghost/i18n/no-unused-ignores', + 0, + 0, + ignores => ignores[rule][index] = null + ); + } + } + + /** @private */ + _applyFixes() { + const newIgnoreFile = structuredClone(this._ignoreFile.parsed); + const resultsToFilter = new Set(); + for (const {fix, resultIndex, messageIndex} of this._fixes) { + this.summary.errorCount -= 1; + fix(newIgnoreFile.overrides); + // @ts-expect-error will be filtered afterwards + this.summary.results[resultIndex].messages[messageIndex] = null; + resultsToFilter.add(resultIndex); + } + + console.log(`Applied ${this._fixes.length} fixes`); // eslint-disable-line no-console + + for (const resultIndex of resultsToFilter) { + this.summary.results[resultIndex].messages = this.summary.results[resultIndex].messages.filter(Boolean); + } + + for (const [rule, ignored] of Object.entries(newIgnoreFile.overrides)) { + const updatedValue = ignored.filter(Boolean); + if (updatedValue.length === 0) { + delete newIgnoreFile.overrides[rule]; + } else { + newIgnoreFile.overrides[rule] = updatedValue; + } + } + + const sortedOverrideKeys = Array.from(Object.keys(newIgnoreFile.overrides)); + sortedOverrideKeys.sort(); + + const newOverrides = {}; + for (const rule of sortedOverrideKeys) { + newOverrides[rule] = newIgnoreFile.overrides[rule]; + } + + newIgnoreFile.overrides = newOverrides; + + writeFileSync(this._ignoreFile.path, JSON.stringify(newIgnoreFile, null, 4)); + } +} + +/** + * Formats and logs the lint results. If there are any errors, the exit code will be 1 + * @param {LintSummary} summary + */ +async function exitWithSummary(summary) { + const formatter = await new ESLint().loadFormatter(); + const formattedResults = await formatter.format(summary.results, {cwd: '', rulesMeta: {}}); + + if (formattedResults) { + console.log(formattedResults); // eslint-disable-line no-console + } + + if (summary.ignoreAllFlagProvidedWithoutFixFlag) { + console.warn( // eslint-disable-line no-console + '--unsafe-ignore-all was provided without --fix; errors were not ignored' + ); + } + + if (summary.errorCount > 0) { + console.log( // eslint-disable-line no-console + `\nNote: Since JSON doesn't support comments, use test/i18n-ignore.json to waive messages.\n\n` + ); + process.exit(1); + } + + process.exit(0); +} + +/** + * Extracts translations variables and any invalid interpolations from a string + * @param {string} string the translation to parse + */ +function parseTranslationString(string) { + const START = 0; + const IN_VARIABLE = 1; + const errors = []; + + let state = START; + let currentVariable = ''; + let variableStartColumn = 0; + const variables = new Map(); + + // Use 1-indexed iterators since column is used a lot more than index + for (let column = 1; column <= string.length; column++) { + const character = string[column - 1]; + if (character === '{') { + if (state === START) { + state = IN_VARIABLE; + // The brace is at the current column; the variable starts at the next column + variableStartColumn = column + 1; + } else { + errors.push({message: 'Unexpected "{" in variable', column}); + } + } else if (character === '}') { + if (state === IN_VARIABLE) { + state = START; + variables.set(currentVariable, variableStartColumn); + currentVariable = ''; + } else { + errors.push({message: 'Unexpected "}" in string', column}); + } + } else if (state === IN_VARIABLE) { + currentVariable += character; + } + } + + if (state === IN_VARIABLE) { + errors.push({message: 'Unclosed {', column: variableStartColumn}); + } + + return {variables, errors}; +} + +async function *getTranslationFiles() { + const globs = await glob(`${LOCALES_ROOT}/*/*.json`); + for (const translationFile of globs) { + yield translationFile; + } +} + +/** + * Adds a translation error to {ignores} + * @param {Record} ignores + * @param {string} rule + * @param {string} file + * @param {string} key + */ +function ignoreTranslationError(ignores, rule, file, key) { + ignores[rule] ??= []; + ignores[rule].push({ + file, + key, + comment: 'FIXME: This error was automatically ignored. Please update the translation or this comment.' + }); +} + +/** + * Reports all errors for a translation pair + * @param {string} key the untranslated key to analyze + * @param {string} translated the translated value to analyze + * @param {LinterContext} context + */ +function analyzeSingleTranslation(key, translated, context) { + const {errors: keyErrors, variables: defines} = parseTranslationString(key); + + // Report key parsing errors only for English translations. + // I18next is responsible for keeping all other locales in sync. + if (context.file?.locale === 'en') { + const {line, column} = context.getPositionForText(key, 'key'); + for (const error of keyErrors) { + const rule = 'ghost/i18n/no-invalid-translations'; + context.reportTranslationError(error.message, key, rule, line, column + error.column); + } + } + + if (!translated) { + return; + } + + const {errors, variables: used} = parseTranslationString(translated); + + { + const {line, column} = context.getPositionForText(key, 'value'); + for (const error of errors) { + const rule = 'ghost/i18n/no-invalid-translations'; + context.reportTranslationError(error.message, key, rule, line, column + error.column); + } + } + + for (const [define, columnInKey] of defines.entries()) { + // Use delete to remove the variable so `used` will only contain unused variables after this loop + if (!used.delete(define)) { + const rule = 'ghost/i18n/no-unused-variables'; + const file = context.relativeFilePath; + const {line, column} = context.getPositionForText(key, 'key'); + context.reportTranslationError( + `Translation does not use variable "${define}"`, + key, + rule, + line, + column + columnInKey, + context.ignoreAll ? ignores => ignoreTranslationError(ignores, rule, file, key) : undefined + ); + } + } + + const {line, column} = context.getPositionForText(translated, 'value'); + for (const [unknownVariable, columnInTranslation] of used.entries()) { + const rule = 'ghost/i18n/no-undefined-variables'; + const file = context.relativeFilePath; + context.reportTranslationError( + `Translation uses unknown variable "${unknownVariable}"`, + key, + rule, + line, + column + columnInTranslation, + context.ignoreAll ? ignores => ignoreTranslationError(ignores, rule, file, key) : undefined + ); + } +} + +async function analyze() { + const context = new LinterContext(path.join(__dirname, './i18n-ignores.json')); + for await (const translationFile of await getTranslationFiles()) { + const file = new File(translationFile, await fs.readFile(translationFile, 'utf8')); + context.setFile(file); + for (const [key, translated] of Object.entries(file.parsed)) { + analyzeSingleTranslation(key, translated, context); + } + } + + context.complete(); + + return context.summary; +} + +if (require.main === module) { + analyze().then(results => exitWithSummary(results)).catch((error) => { + console.error(error); // eslint-disable-line no-console + process.exit(1); + }); +} + +module.exports = { + analyze +}; diff --git a/ghost/i18n/test/i18n.test.js b/ghost/i18n/test/i18n.test.js new file mode 100644 index 0000000..1c5afeb --- /dev/null +++ b/ghost/i18n/test/i18n.test.js @@ -0,0 +1,424 @@ +const assert = require('node:assert/strict'); +const fs = require('fs/promises'); +const path = require('path'); +const fsExtra = require('fs-extra'); +const i18n = require('../'); + +describe('i18n', function () { + it('does not have too-long strings for the Stripe personal note label', async function () { + for (const locale of i18n.SUPPORTED_LOCALES) { + const translationFile = require(path.join(`../locales/`, locale, 'portal.json')); + + if (translationFile['Add a personal note']) { + assert(translationFile['Add a personal note'].length <= 255, `[${locale}/portal.json] Stripe personal note label is too long`); + } + } + }); + + it('is uses default export if available', async function () { + const translationFile = require(path.join(`../locales/`, 'nl', 'portal.json')); + translationFile.Name = undefined; + translationFile.default = { + Name: 'Naam' + }; + + const t = i18n('nl', 'portal').t; + assert.equal(t('Name'), 'Naam'); + }); + + describe('Can use Portal resources', function () { + describe('Dutch', function () { + let t; + + before(function () { + t = i18n('nl', 'portal').t; + }); + + it('can translate `Name`', function () { + assert.equal(t('Name'), 'Naam'); + }); + }); + }); + + describe('Can use Signup-form resources', function () { + describe('Afrikaans', function () { + let t; + + before(function () { + t = i18n('af', 'signup-form').t; + }); + + it('can translate `Now check your email!`', function () { + assert.equal(t('Now check your email!'), 'Kyk nou in jou e-pos!'); + }); + }); + }); + + describe('Fallback when no language is chosen will be english', function () { + describe('English fallback', function () { + let t; + before(function () { + t = i18n().t; + }); + it('can translate with english when no language selected', function () { + assert.equal(t('Back'), 'Back'); + }); + }); + }); + + describe('Fallback will be nb when no is chosen', function () { + describe('Norwegian bokmål fallback', function () { + let t; + before(function () { + t = i18n('no', 'portal').t; + }); + it('Norwegian bokmål used when no is chosen', function () { + assert.equal(t('Yearly'), 'Årlig'); + }); + }); + }); + + describe('Language will be nb when nb is chosen', function () { + describe('Norwegian bokmål', function () { + let t; + before(function () { + t = i18n('nb', 'portal').t; + }); + it('Norwegian bokmål used when "nb" is chosen', function () { + assert.equal(t('Yearly'), 'Årlig'); + }); + }); + }); + + describe('Language is properly "nn" when "nn" is chosen', function () { + describe('Norwegian Nynorsk', function () { + let t; + before(function () { + t = i18n('nn', 'portal').t; + }); + it('Norwegian Nynorsk used when selected', function () { + assert.equal(t('Yearly'), 'Årleg'); + }); + }); + }); + + describe('directories and locales in i18n.js will match', function () { + it('should have a key for each directory in the locales directory', async function () { + const locales = await fs.readdir(path.join(__dirname, '../locales')); + const supportedLocales = i18n.SUPPORTED_LOCALES; + + for (const locale of locales) { + if (locale !== 'context.json') { + assert(supportedLocales.includes(locale), `The locale ${locale} is not in the list of supported locales`); + } + } + }); + + it('should have a directory for each key in lib/i18n.js', async function () { + const supportedLocales = i18n.SUPPORTED_LOCALES; + + for (const locale of supportedLocales) { + const localeDir = path.join(__dirname, `../locales/${locale}`); + const stats = await fs.stat(localeDir); + assert(stats.isDirectory(), `The locale ${locale} does not have a directory`); + } + }); + }); + + describe('newsletter i18n', function () { + it('should be able to translate and interpolate a date', async function () { + const t = i18n('fr', 'ghost').t; + assert.equal(t('Your subscription will renew on {date}.', {date: '8 Oct 2024'}), 'Votre abonnement sera renouvelé le 8 Oct 2024.'); + }); + }); + describe('it gracefully falls back to en if a file is missing', function () { + it('should be able to translate a key that is missing in the locale', async function () { + const resources = i18n.generateResources(['xx'], 'portal'); + const englishResources = i18n.generateResources(['en'], 'portal'); + assert.deepEqual(resources.xx, englishResources.en); + }); + }); + + // The goal of the test below (TODO) is to make sure that new keys get added to context.json with + // enough information to be useful to translators. The person best positioned to do this is + // the person who added the key. However, it's complicated by the order that translate and test + // currently run in, so leaving it disabled for now. + /*describe('context.json is valid', function () { + it('should not contain any empty values', function () { + const context = require('../locales/context.json'); + + function checkForEmptyValues(obj, keypath = '') { + for (const [key, value] of Object.entries(obj)) { + const currentPath = keypath ? `${keypath}.${key}` : key; + + if (value === null || value === undefined || value === '') { + assert.fail(`Empty value found at ${currentPath}. If you added a new key for translation, please add it to the ghost/i18n/locales/context.json file.`); + } + + if (typeof value === 'object' && value !== null) { + checkForEmptyValues(value, currentPath); + } + } + } + + checkForEmptyValues(context); + }); + }); */ + + // i18n theme translations when feature flag is enabled + describe('theme resources', function () { + let themeLocalesPath; + let cleanup; + + beforeEach(async function () { + // Create a temporary theme locales directory + themeLocalesPath = path.join(__dirname, 'temp-theme-locales'); + await fsExtra.ensureDir(themeLocalesPath); + cleanup = async () => { + await fsExtra.remove(themeLocalesPath); + }; + }); + + afterEach(async function () { + await cleanup(); + }); + + it('loads translations from theme locales directory', async function () { + // Create test translation files + const enContent = { + 'Read more': 'Read more', + Subscribe: 'Subscribe' + }; + const frContent = { + 'Read more': 'Lire plus', + Subscribe: 'S\'abonner' + }; + + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + await fsExtra.writeJson(path.join(themeLocalesPath, 'fr.json'), frContent); + + const t = i18n('fr', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Read more'), 'Lire plus'); + assert.equal(t('Subscribe'), 'S\'abonner'); + }); + + it('falls back to en when translation is missing', async function () { + // Create only English translation file + const enContent = { + 'Read more': 'Read more', + Subscribe: 'Subscribe' + }; + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + + const t = i18n('fr', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Read more'), 'Read more'); + assert.equal(t('Subscribe'), 'Subscribe'); + }); + + it('uses empty translations when no files exist', async function () { + const t = i18n('fr', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Read more'), 'Read more'); + assert.equal(t('Subscribe'), 'Subscribe'); + }); + + it('handles invalid JSON files gracefully', async function () { + // Create invalid JSON file + await fsExtra.writeFile(path.join(themeLocalesPath, 'fr.json'), 'invalid json'); + + const t = i18n('fr', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Read more'), 'Read more'); + assert.equal(t('Subscribe'), 'Subscribe'); + }); + + it('handles errors when both requested locale and English fallback files are invalid', async function () { + // Create invalid JSON files for both requested locale and English fallback + await fsExtra.writeFile(path.join(themeLocalesPath, 'de.json'), 'invalid json'); + await fsExtra.writeFile(path.join(themeLocalesPath, 'en.json'), 'also invalid json'); + + const t = i18n('de', 'theme', {themePath: themeLocalesPath}).t; + + // Should fall back to returning the key itself since both files failed + assert.equal(t('Read more'), 'Read more'); + assert.equal(t('Subscribe'), 'Subscribe'); + }); + + it('handles theme files with TypeScript default export structure', async function () { + // Create a theme translation file that mimics TypeScript's default export behavior + // where translations are nested under a 'default' property + const themeContent = { + 'Read more': 'Read more directly', + Subscribe: 'Subscribe directly', + default: { + 'Welcome message': 'Welcome from default', + 'Footer text': 'Footer from default' + } + }; + + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), themeContent); + + const t = i18n('en', 'theme', {themePath: themeLocalesPath}).t; + + // Should be able to access both direct properties and properties from the default export + assert.equal(t('Read more'), 'Read more directly'); + assert.equal(t('Subscribe'), 'Subscribe directly'); + assert.equal(t('Welcome message'), 'Welcome from default'); + assert.equal(t('Footer text'), 'Footer from default'); + }); + + it('handles theme files with non-object default export', async function () { + // Create a theme translation file where the default export is not an object + const themeContent = { + 'Read more': 'Read more', + Subscribe: 'Subscribe', + default: 'not an object' + }; + + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), themeContent); + + const t = i18n('en', 'theme', {themePath: themeLocalesPath}).t; + + // Should only use direct properties, ignoring the non-object default export + assert.equal(t('Read more'), 'Read more'); + assert.equal(t('Subscribe'), 'Subscribe'); + }); + + it('initializes i18next with correct configuration', async function () { + const enContent = { + 'Read more': 'Read more' + }; + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + + const instance = i18n('fr', 'theme', {themePath: themeLocalesPath}); + + // Verify i18next configuration + assert.equal(instance.language, 'fr'); + assert.deepEqual(instance.options.ns, ['theme']); + assert.equal(instance.options.defaultNS, 'theme'); + assert.equal(instance.options.fallbackLng.default[0], 'en'); + assert.equal(instance.options.returnEmptyString, false); + + // Verify resources are loaded correctly + const resources = instance.store.data; + assert(resources.fr); + assert(resources.fr.theme); + assert.equal(resources.fr.theme['Read more'], 'Read more'); + }); + + it('interpolates variables in theme translations', async function () { + const enContent = { + 'Welcome, {name}': 'Welcome, {name}', + 'Hello {firstName} {lastName}': 'Hello {firstName} {lastName}' + }; + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + + const t = i18n('en', 'theme', {themePath: themeLocalesPath}).t; + + // Test simple interpolation + assert.equal(t('Welcome, {name}', {name: 'John'}), 'Welcome, John'); + + // Test multiple variables + assert.equal( + t('Hello {firstName} {lastName}', {firstName: 'John', lastName: 'Doe'}), + 'Hello John Doe' + ); + }); + + it('uses single curly braces for theme namespace interpolation', async function () { + const enContent = { + 'Welcome, {name}': 'Welcome, {name}' + }; + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + + const t = i18n('en', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Welcome, {name}', {name: 'John'}), 'Welcome, John'); + }); + + it('uses single curly braces for portal namespace interpolation', async function () { + const t = i18n('en', 'portal').t; + assert.equal(t('Welcome, {name}', {name: 'John'}), 'Welcome, John'); + }); + + it('uses single curly braces for ghost namespace interpolation', async function () { + const t = i18n('en', 'ghost').t; + assert.equal(t('Welcome, {name}', {name: 'John'}), 'Welcome, John'); + }); + + it('does not html encode interpolated values in the theme namespace', async function () { + const enContent = { + 'Welcome, {name}': 'Welcome, {name}' + }; + await fsExtra.writeJson(path.join(themeLocalesPath, 'en.json'), enContent); + const t = i18n('en', 'theme', {themePath: themeLocalesPath}).t; + assert.equal(t('Welcome, {name}', {name: 'John O\'Nolan'}), 'Welcome, John O\'Nolan'); + }); + }); + + describe('i18next initialization', function () { + it('initializes with correct default configuration', function () { + const instance = i18n('en', 'portal'); + + // Verify basic configuration + assert.equal(instance.language, 'en'); + assert.deepEqual(instance.options.ns, ['portal']); + assert.equal(instance.options.defaultNS, 'portal'); + assert.equal(instance.options.fallbackLng.default[0], 'en'); + assert.equal(instance.options.returnEmptyString, false); + assert.equal(instance.options.nsSeparator, false); + assert.equal(instance.options.keySeparator, false); + + // Verify interpolation configuration for portal namespace + assert.equal(instance.options.interpolation.prefix, '{'); + assert.equal(instance.options.interpolation.suffix, '}'); + }); + + it('initializes with correct theme configuration', function () { + const instance = i18n('en', 'theme', {themePath: '/path/to/theme'}); + + // Verify basic configuration + assert.equal(instance.language, 'en'); + assert.deepEqual(instance.options.ns, ['theme']); + assert.equal(instance.options.defaultNS, 'theme'); + assert.equal(instance.options.fallbackLng.default[0], 'en'); + assert.equal(instance.options.returnEmptyString, false); + assert.equal(instance.options.nsSeparator, false); + assert.equal(instance.options.keySeparator, false); + + // Verify interpolation configuration for theme namespace + assert.equal(instance.options.interpolation.prefix, '{'); + assert.equal(instance.options.interpolation.suffix, '}'); + }); + + it('initializes with correct newsletter (now ghost) configuration', function () { + // note: we just merged newsletter into Ghost, so there might be some redundancy here + const instance = i18n('en', 'ghost'); + + // Verify basic configuration + assert.equal(instance.language, 'en'); + assert.deepEqual(instance.options.ns, ['ghost']); + assert.equal(instance.options.defaultNS, 'ghost'); + assert.equal(instance.options.fallbackLng.default[0], 'en'); + assert.equal(instance.options.returnEmptyString, false); + assert.equal(instance.options.nsSeparator, false); + assert.equal(instance.options.keySeparator, false); + + // Verify interpolation configuration for ghost namespace + assert.equal(instance.options.interpolation.prefix, '{'); + assert.equal(instance.options.interpolation.suffix, '}'); + }); + + it('initializes with correct fallback language configuration', function () { + const instance = i18n('no', 'portal'); + + // Verify Norwegian fallback chain + assert.deepEqual(instance.options.fallbackLng.no, ['nb', 'en']); + assert.deepEqual(instance.options.fallbackLng.default, ['en']); + }); + + it('initializes with empty theme resources when no theme path provided', function () { + const instance = i18n('en', 'theme'); + + // Verify empty theme resources + assert.deepEqual(instance.store.data.en.theme, {}); + }); + }); +}); diff --git a/ghost/i18n/test/utils.js b/ghost/i18n/test/utils.js new file mode 100644 index 0000000..16ca15f --- /dev/null +++ b/ghost/i18n/test/utils.js @@ -0,0 +1,46 @@ +function extractVariables(str) { + if (!str) { + return new Set(); + } + const regex = /\{([^}]+)\}/g; + const variables = new Set(); + let match; + while ((match = regex.exec(str)) !== null) { + variables.add(match[1]); + } + return variables; +} + +function checkTranslationPair(key, value) { + let result = []; + // Skip checking if value is an empty string + if (value === '') { + return result; + } + + if (typeof key !== 'string') { + return result; + } + + const keyVars = extractVariables(key); + const valueVars = extractVariables(value); + + // Check for variables in key but not in value + for (const keyVar of keyVars) { + if (!valueVars.has(keyVar)) { + result.push('missingVariable'); + } + } + + // Check for variables in value but not in key + for (const valueVar of valueVars) { + if (!keyVars.has(valueVar)) { + result.push('addedVariable'); + } + } + return result; +} + +module.exports = { + checkTranslationPair +}; diff --git a/ghost/parse-email-address/.eslintrc.js b/ghost/parse-email-address/.eslintrc.js new file mode 100644 index 0000000..ecc2852 --- /dev/null +++ b/ghost/parse-email-address/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/parse-email-address/LICENSE b/ghost/parse-email-address/LICENSE new file mode 100644 index 0000000..efad547 --- /dev/null +++ b/ghost/parse-email-address/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2026 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ghost/parse-email-address/README.md b/ghost/parse-email-address/README.md new file mode 100644 index 0000000..3be097f --- /dev/null +++ b/ghost/parse-email-address/README.md @@ -0,0 +1,36 @@ +# Parse Email Address + +Extract the local and domain parts of email address strings. + +## Usage + +```javascript +parseEmailAddress('foo@example.com'); +// => {local: 'foo', domain: 'example.com'} + +parseEmailAddress('invalid'); +// => null + +parseEmailAddress('foo@中文.example'); +// => {local: 'foo', domain: 'xn--fiq228c.example'} +``` + +- Domain names must have at least two labels. `example.com` is okay, `example` is not. +- The top level domain must have at least two octets. `example.com` is okay, `example.x` is not. +- There are various length limits: + - The whole email is limited to 986 octets, per SMTP. + - Domain names are limited to 253 octets, per SMTP. + - Domain labels are limited to 63 octets, per DNS. + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `pnpm` to install top-level dependencies. + +## Test + +- `pnpm lint` run just eslint +- `pnpm test` run lint and tests \ No newline at end of file diff --git a/ghost/parse-email-address/package.json b/ghost/parse-email-address/package.json new file mode 100644 index 0000000..5f3a717 --- /dev/null +++ b/ghost/parse-email-address/package.json @@ -0,0 +1,46 @@ +{ + "name": "@tryghost/parse-email-address", + "version": "0.0.0", + "private": true, + "repository": "https://github.com/TryGhost/Ghost/tree/main/ghost/parse-email-address", + "author": "Ghost Foundation", + "license": "MIT", + "main": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "dev": "tsc --watch --preserveWatchOutput --sourceMap", + "build": "pnpm build:tsc", + "build:tsc": "tsc", + "test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --lines 100 --functions 100 --statements 100 --branches 80 --reporter text --reporter cobertura mocha --node-option import=tsx './test/**/*.test.ts'", + "test": "pnpm test:types && pnpm test:unit", + "test:types": "tsc --noEmit", + "lint:code": "eslint src/ --ext .ts --cache", + "lint": "pnpm lint:code && pnpm lint:test", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache" + }, + "files": [ + "build" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/node": "25.6.0", + "c8": "10.1.3", + "mocha": "11.7.5", + "sinon": "21.0.1", + "ts-node": "10.9.2", + "tsx": "4.21.0", + "typescript": "5.9.3" + }, + "dependencies": { + "parse-email-address": "0.0.2" + }, + "nx": { + "targets": { + "build": { + "outputs": ["{projectRoot}/build"] + } + } + } +} diff --git a/ghost/parse-email-address/src/index.ts b/ghost/parse-email-address/src/index.ts new file mode 100644 index 0000000..e8c42ab --- /dev/null +++ b/ghost/parse-email-address/src/index.ts @@ -0,0 +1,20 @@ +import {parseEmailAddress as upstreamParseEmailAddress} from 'parse-email-address'; +import {domainToASCII} from 'node:url'; + +export const parseEmailAddress = ( + emailAddress: string +): null | { local: string; domain: string } => { + const upstreamParsed = upstreamParseEmailAddress(emailAddress); + if (!upstreamParsed) { + return null; + } + + const {user: local, domain: rawDomain} = upstreamParsed; + + const domain = domainToASCII(rawDomain); + if (!domain) { + return null; + } + + return {local, domain}; +}; diff --git a/ghost/parse-email-address/test/.eslintrc.js b/ghost/parse-email-address/test/.eslintrc.js new file mode 100644 index 0000000..6fe6dc1 --- /dev/null +++ b/ghost/parse-email-address/test/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/parse-email-address/test/index.test.ts b/ghost/parse-email-address/test/index.test.ts new file mode 100644 index 0000000..fee310b --- /dev/null +++ b/ghost/parse-email-address/test/index.test.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import {parseEmailAddress} from '../src'; + +describe('parseEmailAddress', function () { + it('returns null for invalid email addresses', function () { + const invalid = [ + // These aren't email addresses + '', + 'foo', + 'example.com', + '@example.com', + // Bad spacing + ' foo@example.com', + 'foo@example.com ', + 'foo @example.com', + 'foo@example .com', + // Too many @s + 'foo@bar@example.com', + // Invalid user + 'a"b(c)d,e:f;gi[j\\k]l@example.com', + 'just"not"right@example.com', + 'this is"not\\allowed@example.com', + 'x'.repeat(975) + '@example.com', + // Invalid domains + 'foo@example', + 'foo@no_underscores.example', + 'foo@xn--iñvalid.com', + 'foo@' + 'x'.repeat(253) + '.yz', + // IP domains + 'foo@[127.0.0.1]', + 'foo@[IPv6:::1]', + 'foo@[ipv6:::1]', + // Tag domains + 'foo@[bar:Baz]' + ]; + for (const input of invalid) { + assert.equal(parseEmailAddress(input), null, input); + } + }); + + it('returns the local and domain part of domains', function () { + const testCases: [string, string, string][] = [ + // Basic cases + ['foo@example.com', 'foo', 'example.com'], + ['foo.bar@example.com', 'foo.bar', 'example.com'], + ['foo.bar+baz@example.com', 'foo.bar+baz', 'example.com'], + // Unusual usernames + ['" "@example.com', '" "', 'example.com'], + ['"foo..bar"@example.com', '"foo..bar"', 'example.com'], + ['""@example.com', '""', 'example.com'], + ['"\\"@example.com', '"\\"', 'example.com'], + ['"foo@bar.example"@example.com', '"foo@bar.example"', 'example.com'], + // Lowercasing + ['Foo@Example.COM', 'Foo', 'example.com'], + // Non-ASCII + ['foo@中文.example', 'foo', 'xn--fiq228c.example'], + ['中文@example.com', '中文', 'example.com'] + ]; + for (const [input, local, domain] of testCases) { + assert.deepEqual(parseEmailAddress(input), {local, domain}, input); + } + }); +}); diff --git a/ghost/parse-email-address/tsconfig.json b/ghost/parse-email-address/tsconfig.json new file mode 100644 index 0000000..5392039 --- /dev/null +++ b/ghost/parse-email-address/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ + "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ + "erasableSyntaxOnly": true, /* Require all TypeScript code to be erasable. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"] +} diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..190af49 --- /dev/null +++ b/nx.json @@ -0,0 +1,41 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "namedInputs": { + "default": ["{projectRoot}/**/*", "{workspaceRoot}/ghost/tsconfig.json"] + }, + "parallel": 4, + "targetDefaults": { + "build": { + "dependsOn": ["^build"], + "inputs": [ + { + "env": "GHOST_CDN_URL" + }, + "default", + "^default" + ], + "outputs": [ + "{projectRoot}/dist", + "{projectRoot}/es", + "{projectRoot}/types", + "{projectRoot}/umd" + ], + "cache": true + }, + "lint": { + "cache": true + }, + "test": { + "cache": true + }, + "test:unit": { + "cache": true, + "dependsOn": ["build"] + }, + "dev": { + "dependsOn": ["^dev"], + "continuous": true + } + }, + "cacheDirectory": ".nxcache" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..84b845a --- /dev/null +++ b/package.json @@ -0,0 +1,161 @@ +{ + "name": "ghost-monorepo", + "version": "0.0.0-private", + "description": "The professional publishing platform", + "private": true, + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", + "repository": "https://github.com/TryGhost/Ghost", + "author": "Ghost Foundation", + "license": "MIT", + "monorepo": { + "public": false, + "internalPackages": true, + "repo": "https://github.com/TryGhost/Ghost", + "scope": "@tryghost" + }, + "eslintIgnore": [ + "**/node_modules/**" + ], + "scripts": { + "archive": "pnpm nx run ghost:archive", + "build:production": "pnpm nx run ghost:build:tsc && pnpm nx run ghost:build:assets && pnpm nx run @tryghost/admin:build", + "build": "pnpm nx run-many -t build", + "build:clean": "nx reset && rimraf -g 'ghost/*/build' && rimraf -g 'ghost/*/tsconfig.tsbuildinfo'", + "clean:hard": "node ./.github/scripts/clean.js", + "dev": "pnpm nx run ghost-monorepo:docker:dev", + "dev:sqlite": "DEV_COMPOSE_FILES='-f compose.dev.sqlite.yaml' pnpm nx run ghost-monorepo:docker:dev", + "dev:mailgun": "DEV_COMPOSE_FILES='-f compose.dev.mailgun.yaml' pnpm nx run ghost-monorepo:docker:dev", + "dev:lexical": "EDITOR_URL=http://localhost:2368/ghost/assets/koenig-lexical/ pnpm dev", + "dev:analytics": "DEV_COMPOSE_FILES='-f compose.dev.analytics.yaml' pnpm nx run ghost-monorepo:docker:dev", + "dev:storage": "DEV_COMPOSE_FILES='-f compose.dev.storage.yaml' pnpm nx run ghost-monorepo:docker:dev", + "dev:stripe": "./docker/stripe/with-stripe.sh pnpm nx run ghost-monorepo:docker:dev", + "dev:all": "DEV_COMPOSE_FILES='-f compose.dev.analytics.yaml -f compose.dev.storage.yaml' ./docker/stripe/with-stripe.sh pnpm nx run ghost-monorepo:docker:dev", + "fix": "pnpm store prune && rimraf -g '**/node_modules' && pnpm install && pnpm nx reset", + "knex-migrator": "pnpm --filter ghost run knex-migrator", + "setup": "pnpm install && git submodule update --init --recursive", + "reset:data": "docker exec ghost-dev bash -c 'cd /home/ghost/ghost/core && node index.js generate-data --clear-database --quantities members:1000,posts:100 --seed 123'", + "reset:data:empty": "docker exec ghost-dev bash -c 'cd /home/ghost/ghost/core && node index.js generate-data --clear-database --quantities members:0,posts:0 --seed 123'", + "reset:data:xxl": "docker exec ghost-dev bash -c 'cd /home/ghost/ghost/core && node index.js generate-data --clear-database --quantities members:2000000,posts:0,emails:0,members_stripe_customers:0,members_login_events:0,members_status_events:0 --seed 123'", + "docker:build": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} build", + "docker:clean": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} --profile all down -v --remove-orphans --rmi local", + "docker:down": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} down", + "lint": "pnpm nx run-many -t lint", + "test": "pnpm nx run-many -t test --exclude @tryghost/e2e --exclude ghost-admin", + "test:unit": "pnpm nx run-many -t test:unit", + "test:e2e": "pnpm --filter @tryghost/e2e test", + "test:e2e:analytics": "pnpm --filter @tryghost/e2e test:analytics", + "test:e2e:all": "pnpm --filter @tryghost/e2e test:all", + "test:e2e:debug": "DEBUG=@tryghost/e2e:* pnpm test:e2e", + "main": "pnpm main:monorepo && pnpm main:submodules", + "main:monorepo": "git checkout main && git pull ${GHOST_UPSTREAM:-origin} main && pnpm install", + "main:submodules": "git submodule sync && git submodule update && git submodule foreach \"git checkout main && git pull ${GHOST_UPSTREAM:-origin} main\"", + "preinstall": "node ./.github/scripts/enforce-package-manager.js", + "prepare": "husky .github/hooks", + "tb": "tb local start && cd ghost/core/core/server/data/tinybird && tb dev", + "tb:install": "curl https://tinybird.co | sh", + "data:analytics:generate": "node ghost/core/core/server/data/tinybird/scripts/docker-analytics-manager.js generate", + "data:analytics:clear": "node ghost/core/core/server/data/tinybird/scripts/docker-analytics-manager.js clear", + "release": "node scripts/release.js", + "test:release": "node --test 'scripts/test/*.js'" + }, + "pnpm": { + "overrides": { + "@tryghost/errors": "^1.3.7", + "@tryghost/logging": "2.5.5", + "jackspeak": "2.3.6", + "moment": "2.24.0", + "moment-timezone": "0.5.45", + "nwsapi": "2.2.23", + "broccoli-persistent-filter": "^2.3.1", + "juice": "9.1.0", + "ember-basic-dropdown": "6.0.2", + "ember-in-viewport": "4.1.0", + "eslint-plugin-ghost>@typescript-eslint/eslint-plugin": "8.49.0", + "eslint-plugin-ghost>@typescript-eslint/utils": "8.49.0", + "ember-svg-jar>cheerio": "1.0.0-rc.12", + "juice>cheerio": "0.22.0", + "lodash.template": "4.5.0" + }, + "onlyBuiltDependencies": [ + "@swc/core", + "core-js", + "cpu-features", + "dtrace-provider", + "esbuild", + "fsevents", + "msw", + "nx", + "protobufjs", + "re2", + "sharp", + "sqlite3", + "ssh2" + ] + }, + "devDependencies": { + "@actions/core": "3.0.0", + "@playwright/test": "1.59.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "concurrently": "8.2.2", + "eslint": "catalog:", + "eslint-plugin-ghost": "3.5.0", + "eslint-plugin-react": "7.37.5", + "husky": "9.1.7", + "inquirer": "8.2.7", + "jsonc-parser": "3.3.1", + "lint-staged": "16.4.0", + "nx": "22.0.4", + "rimraf": "6.1.3", + "semver": "7.7.4", + "typescript": "5.9.3" + }, + "nx": { + "includedScripts": [], + "targets": { + "docker:up": { + "executor": "nx:run-commands", + "options": { + "command": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} up -d --force-recreate --wait" + }, + "dependsOn": [ + "docker:build" + ] + }, + "docker:down": { + "executor": "nx:run-commands", + "options": { + "command": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} down" + } + }, + "docker:build": { + "executor": "nx:run-commands", + "options": { + "command": "docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} build" + } + }, + "docker:dev": { + "continuous": true, + "executor": "nx:run-commands", + "options": { + "command": "trap 'docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} down' EXIT; docker compose -f compose.dev.yaml ${DEV_COMPOSE_FILES} logs -f" + }, + "dependsOn": [ + "docker:up", + { + "target": "dev", + "projects": [ + "@tryghost/admin", + "@tryghost/portal", + "@tryghost/comments-ui", + "@tryghost/signup-form", + "@tryghost/sodo-search", + "@tryghost/announcement-bar", + "@tryghost/parse-email-address" + ] + } + ] + } + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d51da54 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,50343 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +catalogs: + default: + '@eslint/js': + specifier: 8.57.1 + version: 8.57.1 + eslint: + specifier: 8.57.1 + version: 8.57.1 + eslint9: + '@eslint/js': + specifier: 9.37.0 + version: 9.37.0 + eslint: + specifier: 9.37.0 + version: 9.37.0 + +overrides: + '@tryghost/errors': ^1.3.7 + '@tryghost/logging': 2.5.5 + jackspeak: 2.3.6 + moment: 2.24.0 + moment-timezone: 0.5.45 + nwsapi: 2.2.23 + broccoli-persistent-filter: ^2.3.1 + juice: 9.1.0 + ember-basic-dropdown: 6.0.2 + ember-in-viewport: 4.1.0 + eslint-plugin-ghost>@typescript-eslint/eslint-plugin: 8.49.0 + eslint-plugin-ghost>@typescript-eslint/utils: 8.49.0 + ember-svg-jar>cheerio: 1.0.0-rc.12 + juice>cheerio: 0.22.0 + lodash.template: 4.5.0 + +importers: + + .: + devDependencies: + '@actions/core': + specifier: 3.0.0 + version: 3.0.0 + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + chalk: + specifier: 4.1.2 + version: 4.1.2 + chokidar: + specifier: 3.6.0 + version: 3.6.0 + concurrently: + specifier: 8.2.2 + version: 8.2.2 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-ghost: + specifier: 3.5.0 + version: 3.5.0(@babel/core@7.29.0)(eslint@8.57.1) + eslint-plugin-react: + specifier: 7.37.5 + version: 7.37.5(eslint@8.57.1) + husky: + specifier: 9.1.7 + version: 9.1.7 + inquirer: + specifier: 8.2.7 + version: 8.2.7(@types/node@25.6.0) + jsonc-parser: + specifier: 3.3.1 + version: 3.3.1 + lint-staged: + specifier: 16.4.0 + version: 16.4.0 + nx: + specifier: 22.0.4 + version: 22.0.4(@swc/core@1.15.21) + rimraf: + specifier: 6.1.3 + version: 6.1.3 + semver: + specifier: 7.7.4 + version: 7.7.4 + typescript: + specifier: 5.9.3 + version: 5.9.3 + + apps/activitypub: + dependencies: + '@hookform/resolvers': + specifier: 5.2.2 + version: 5.2.2(react-hook-form@7.72.1(react@18.3.1)) + '@radix-ui/react-form': + specifier: 0.1.8 + version: 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../admin-x-framework + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + clsx: + specifier: 2.1.1 + version: 2.1.1 + dompurify: + specifier: 3.3.1 + version: 3.3.1 + html2canvas-objectfit-fix: + specifier: 1.2.0 + version: 1.2.0 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: 7.72.1 + version: 7.72.1(react@18.3.1) + react-router: + specifier: 7.14.0 + version: 7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: 2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-debounce: + specifier: 10.1.1 + version: 10.1.1(react@18.3.1) + zod: + specifier: 4.1.12 + version: 4.1.12 + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/dompurify': + specifier: 3.2.0 + version: 3.2.0 + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + jest: + specifier: 29.7.0 + version: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + ts-jest: + specifier: 29.4.9 + version: 29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/admin: + dependencies: + '@tryghost/activitypub': + specifier: workspace:* + version: link:../activitypub + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../admin-x-framework + '@tryghost/admin-x-settings': + specifier: workspace:* + version: link:../admin-x-settings + '@tryghost/koenig-lexical': + specifier: 1.7.30 + version: 1.7.30 + '@tryghost/posts': + specifier: workspace:* + version: link:../posts + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + '@tryghost/stats': + specifier: workspace:* + version: link:../stats + mingo: + specifier: 2.5.3 + version: 2.5.3 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + zod: + specifier: 4.1.12 + version: 4.1.12 + devDependencies: + '@eslint/js': + specifier: catalog:eslint9 + version: 9.37.0 + '@tailwindcss/vite': + specifier: 4.2.1 + version: 4.2.1(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/node': + specifier: 25.6.0 + version: 25.6.0 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react-swc': + specifier: 4.1.0 + version: 4.1.0(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + eslint: + specifier: catalog:eslint9 + version: 9.37.0(jiti@2.6.1) + eslint-plugin-no-relative-import-paths: + specifier: 1.6.1 + version: 1.6.1 + eslint-plugin-react-hooks: + specifier: 5.2.0 + version: 5.2.0(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-tailwindcss: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0(tailwindcss@4.2.2) + globals: + specifier: 17.4.0 + version: 17.4.0 + jest-extended: + specifier: 7.0.0 + version: 7.0.0(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3) + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + msw: + specifier: 2.12.14 + version: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + sirv: + specifier: 3.0.2 + version: 3.0.2 + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + typescript: + specifier: 5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: 8.58.0 + version: 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + vite: + specifier: 7.1.12 + version: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-tsconfig-paths: + specifier: 5.1.4 + version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: + specifier: 4.1.2 + version: 4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + + apps/admin-x-design-system: + dependencies: + '@dnd-kit/core': + specifier: 6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: 7.0.2 + version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@ebay/nice-modal-react': + specifier: 1.2.13 + version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: 1.1.11 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: 1.3.3 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-form': + specifier: 0.1.8 + version: 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: 1.3.8 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: 1.1.8 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-switch': + specifier: 1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: 1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: 1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/react': + specifier: 7.120.4 + version: 7.120.4(react@18.3.1) + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + '@uiw/react-codemirror': + specifier: 4.25.2 + version: 4.25.2(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@5.48.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: + specifier: 2.1.1 + version: 2.1.1 + react-colorful: + specifier: 5.6.1 + version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-hot-toast: + specifier: 2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-select: + specifier: 5.10.2 + version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@codemirror/lang-html': + specifier: 6.4.11 + version: 6.4.11 + '@codemirror/state': + specifier: 6.6.0 + version: 6.6.0 + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) + '@storybook/addon-essentials': + specifier: 8.6.14 + version: 8.6.14(@types/react@18.3.28)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-interactions': + specifier: 8.6.14 + version: 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-links': + specifier: 8.6.14 + version: 8.6.14(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-styling': + specifier: 1.3.7 + version: 1.3.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(encoding@0.1.13)(less@4.6.4)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + '@storybook/blocks': + specifier: 8.6.14 + version: 8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/preview-api': + specifier: ^8.6.14 + version: 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/react': + specifier: 8.6.14 + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3) + '@storybook/react-vite': + specifier: 8.6.14 + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.0)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@storybook/testing-library': + specifier: 0.2.2 + version: 0.2.2 + '@tailwindcss/postcss': + specifier: 4.2.1 + version: 4.2.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/react-hooks': + specifier: 8.0.1 + version: 8.0.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/lodash-es': + specifier: 4.17.12 + version: 4.17.12 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@types/validator': + specifier: 13.15.10 + version: 13.15.10 + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.6) + c8: + specifier: 10.1.3 + version: 10.1.3 + chai: + specifier: 4.5.0 + version: 4.5.0 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + eslint-plugin-tailwindcss: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0(tailwindcss@4.2.1) + glob: + specifier: ^10.5.0 + version: 10.5.0 + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + lodash-es: + specifier: 4.18.1 + version: 4.18.1 + postcss: + specifier: 8.5.6 + version: 8.5.6 + postcss-import: + specifier: 16.1.1 + version: 16.1.1(postcss@8.5.6) + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + rollup-plugin-node-builtins: + specifier: 2.1.2 + version: 2.1.2 + sinon: + specifier: 18.0.1 + version: 18.0.1 + storybook: + specifier: 8.6.15 + version: 8.6.15(prettier@2.8.8) + tailwindcss: + specifier: 4.2.1 + version: 4.2.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + validator: + specifier: 13.12.0 + version: 13.12.0 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/admin-x-framework: + dependencies: + '@ebay/nice-modal-react': + specifier: 1.2.13 + version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/react': + specifier: 7.120.4 + version: 7.120.4(react@18.3.1) + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tinybirdco/charts': + specifier: 0.2.4 + version: 0.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/admin-x-design-system': + specifier: workspace:* + version: link:../admin-x-design-system + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-hot-toast: + specifier: 2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: + specifier: 7.14.0 + version: 7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@testing-library/jest-dom': + specifier: 5.17.0 + version: 5.17.0 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/koenig-lexical': + specifier: 1.7.30 + version: 1.7.30 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + c8: + specifier: 10.1.3 + version: 10.1.3 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + glob: + specifier: ^10.5.0 + version: 10.5.0 + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + msw: + specifier: 2.12.14 + version: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + sinon: + specifier: 18.0.1 + version: 18.0.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-css-injected-by-js: + specifier: 3.5.2 + version: 3.5.2(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/admin-x-settings: + dependencies: + '@codemirror/lang-html': + specifier: 6.4.11 + version: 6.4.11 + '@dnd-kit/sortable': + specifier: 7.0.2 + version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@ebay/nice-modal-react': + specifier: 1.2.13 + version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/react': + specifier: 7.120.4 + version: 7.120.4(react@18.3.1) + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/color-utils': + specifier: 0.2.16 + version: 0.2.16 + '@tryghost/i18n': + specifier: workspace:* + version: link:../../ghost/i18n + '@tryghost/kg-unsplash-selector': + specifier: 0.3.26 + version: 0.3.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/limit-service': + specifier: 1.5.2 + version: 1.5.2 + '@tryghost/nql': + specifier: 0.12.10 + version: 0.12.10 + '@tryghost/timezone-data': + specifier: 0.4.18 + version: 0.4.18 + '@uiw/react-codemirror': + specifier: 4.25.2 + version: 4.25.2(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@5.48.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: + specifier: 2.1.1 + version: 2.1.1 + lucide-react: + specifier: 0.577.0 + version: 0.577.0(react@18.3.1) + mingo: + specifier: 2.5.3 + version: 2.5.3 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-hot-toast: + specifier: 2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-select: + specifier: 5.10.2 + version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: 2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + validator: + specifier: 13.12.0 + version: 13.12.0 + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@testing-library/jest-dom': + specifier: ^6 + version: 6.9.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tryghost/admin-x-design-system': + specifier: workspace:* + version: link:../admin-x-design-system + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../admin-x-framework + '@tryghost/custom-fonts': + specifier: 1.0.8 + version: 1.0.8 + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + '@types/node': + specifier: 22.19.17 + version: 22.19.17 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@types/validator': + specifier: 13.15.10 + version: 13.15.10 + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + eslint-plugin-tailwindcss: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0(tailwindcss@4.2.2) + stylelint: + specifier: 15.11.0 + version: 15.11.0(typescript@5.9.3) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-css-injected-by-js: + specifier: 3.5.2 + version: 3.5.2(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@22.19.17)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/announcement-bar: + dependencies: + '@tryghost/content-api': + specifier: 1.12.6 + version: 1.12.6 + react: + specifier: 17.0.2 + version: 17.0.2 + react-dom: + specifier: 17.0.2 + version: 17.0.2(react@17.0.2) + devDependencies: + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: ~3.2.4 + version: 3.2.4(vitest@3.2.4) + cross-fetch: + specifier: 4.1.0 + version: 4.1.0(encoding@0.1.13) + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 3.2.4 + version: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + apps/comments-ui: + dependencies: + '@doist/react-interpolate': + specifier: 2.2.1 + version: 2.2.1(prop-types@15.8.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@headlessui/react': + specifier: 1.7.19 + version: 1.7.19(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@tiptap/core': + specifier: 2.26.3 + version: 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/extension-blockquote': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-document': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-hard-break': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-link': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-paragraph': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/extension-placeholder': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-text': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3)) + '@tiptap/pm': + specifier: 2.26.3 + version: 2.26.3 + '@tiptap/react': + specifier: 2.26.3 + version: 2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + react: + specifier: 17.0.2 + version: 17.0.2 + react-dom: + specifier: 17.0.2 + version: 17.0.2(react@17.0.2) + react-string-replace: + specifier: 1.1.1 + version: 1.1.1 + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@testing-library/jest-dom': + specifier: 5.17.0 + version: 5.17.0 + '@testing-library/react': + specifier: 12.1.5 + version: 12.1.5(@types/react@18.3.28)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.0) + '@tryghost/i18n': + specifier: workspace:* + version: link:../../ghost/i18n + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: 0.34.6 + version: 0.34.6(vitest@1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.6) + bson-objectid: + specifier: 2.0.4 + version: 2.0.4 + concurrently: + specifier: 8.2.2 + version: 8.2.2 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-i18next: + specifier: 6.1.3 + version: 6.1.3 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + eslint-plugin-tailwindcss: + specifier: 3.18.2 + version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + moment: + specifier: 2.24.0 + version: 2.24.0 + postcss: + specifier: 8.5.6 + version: 8.5.6 + sinon: + specifier: ^21.1.1 + version: 21.1.1 + tailwindcss: + specifier: 3.4.18 + version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-css-injected-by-js: + specifier: 3.5.2 + version: 3.5.2(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/portal: + dependencies: + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + devDependencies: + '@babel/eslint-parser': + specifier: 7.28.4 + version: 7.28.4(@babel/core@7.29.0)(eslint@8.57.1) + '@doist/react-interpolate': + specifier: 2.2.1 + version: 2.2.1(prop-types@15.8.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@sentry/react': + specifier: 7.120.4 + version: 7.120.4(react@17.0.2) + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 12.1.5 + version: 12.1.5(@types/react@18.3.28)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@testing-library/user-event': + specifier: 14.6.1 + version: 14.6.1(@testing-library/dom@10.4.0) + '@tryghost/i18n': + specifier: workspace:* + version: link:../../ghost/i18n + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4) + '@vitest/ui': + specifier: 3.2.4 + version: 3.2.4(vitest@3.2.4) + concurrently: + specifier: 8.2.2 + version: 8.2.2 + cross-fetch: + specifier: 4.1.0 + version: 4.1.0(encoding@0.1.13) + dompurify: + specifier: 3.3.1 + version: 3.3.1 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-i18next: + specifier: 6.1.3 + version: 6.1.3 + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + react: + specifier: 17.0.2 + version: 17.0.2 + react-dom: + specifier: 17.0.2 + version: 17.0.2(react@17.0.2) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-css-injected-by-js: + specifier: 3.5.2 + version: 3.5.2(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 3.2.4 + version: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + apps/posts: + dependencies: + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../admin-x-framework + '@tryghost/nql-lang': + specifier: 0.6.4 + version: 0.6.4 + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + i18n-iso-countries: + specifier: 7.14.0 + version: 7.14.0 + moment: + specifier: 2.24.0 + version: 2.24.0 + moment-timezone: + specifier: 0.5.45 + version: 0.5.45 + papaparse: + specifier: 5.5.3 + version: 5.5.3 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-router: + specifier: 7.14.0 + version: 7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: 2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-debounce: + specifier: 10.1.1 + version: 10.1.1(react@18.3.1) + zod: + specifier: 4.1.12 + version: 4.1.12 + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': + specifier: 3.13.23 + version: 3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/jest-dom': + specifier: ^6 + version: 6.9.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + msw: + specifier: 2.12.14 + version: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/shade: + dependencies: + '@dnd-kit/core': + specifier: 6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: 7.0.2 + version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@ebay/nice-modal-react': + specifier: 1.2.13 + version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@hookform/resolvers': + specifier: 5.2.2 + version: 5.2.2(react-hook-form@7.72.1(react@18.3.1)) + '@number-flow/react': + specifier: 0.5.10 + version: 0.5.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-accordion': + specifier: 1.2.12 + version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: 1.1.11 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: 1.3.3 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: 2.1.16 + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-form': + specifier: 0.1.8 + version: 0.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: 2.1.8 + version: 2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: 1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: 1.3.8 + version: 1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: 2.2.6 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: 1.1.8 + version: 1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: 1.3.6 + version: 1.3.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: 1.2.4 + version: 1.2.4(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-switch': + specifier: 1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: 1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: 1.1.10 + version: 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: 1.1.11 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: 1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/react': + specifier: 7.120.4 + version: 7.120.4(react@18.3.1) + '@types/color': + specifier: 4.2.1 + version: 4.2.1 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@types/validator': + specifier: 13.15.10 + version: 13.15.10 + '@uiw/react-codemirror': + specifier: 4.25.2 + version: 4.25.2(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@5.48.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + class-variance-authority: + specifier: 0.7.1 + version: 0.7.1 + clsx: + specifier: 2.1.1 + version: 2.1.1 + cmdk: + specifier: 1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + color: + specifier: ^5.0.3 + version: 5.0.3 + lucide-react: + specifier: 0.577.0 + version: 0.577.0(react@18.3.1) + moment-timezone: + specifier: 0.5.45 + version: 0.5.45 + next-themes: + specifier: 0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: 18.3.1 + version: 18.3.1 + react-colorful: + specifier: 5.6.1 + version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: 14.2.3 + version: 14.2.3(react@18.3.1) + react-hook-form: + specifier: 7.72.1 + version: 7.72.1(react@18.3.1) + react-hot-toast: + specifier: 2.6.0 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-select: + specifier: 5.10.2 + version: 5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-world-flags: + specifier: 1.6.0 + version: 1.6.0(react@18.3.1) + recharts: + specifier: 2.15.4 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: 2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: 3.5.0 + version: 3.5.0 + validator: + specifier: 13.12.0 + version: 13.12.0 + zod: + specifier: 4.1.12 + version: 4.1.12 + devDependencies: + '@codemirror/lang-html': + specifier: 6.4.11 + version: 6.4.11 + '@storybook/addon-docs': + specifier: 10.3.3 + version: 10.3.3(@types/react@18.3.28)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + '@storybook/addon-links': + specifier: 10.3.3 + version: 10.3.3(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@storybook/react-vite': + specifier: 10.3.3 + version: 10.3.3(esbuild@0.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + '@tailwindcss/postcss': + specifier: 4.2.1 + version: 4.2.1 + '@tailwindcss/vite': + specifier: 4.2.1 + version: 4.2.1(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/react-hooks': + specifier: 8.0.1 + version: 8.0.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/lodash-es': + specifier: 4.17.12 + version: 4.17.12 + '@types/node': + specifier: 22.19.17 + version: 22.19.17 + '@types/react-world-flags': + specifier: 1.6.0 + version: 1.6.0 + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + c8: + specifier: 10.1.3 + version: 10.1.3 + chai: + specifier: 4.5.0 + version: 4.5.0 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + eslint-plugin-storybook: + specifier: 10.3.3 + version: 10.3.3(eslint@8.57.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + eslint-plugin-tailwindcss: + specifier: 4.0.0-beta.0 + version: 4.0.0-beta.0(tailwindcss@4.2.1) + glob: + specifier: ^10.5.0 + version: 10.5.0 + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + lodash-es: + specifier: 4.18.1 + version: 4.18.1 + postcss: + specifier: 8.5.6 + version: 8.5.6 + rollup-plugin-node-builtins: + specifier: 2.1.2 + version: 2.1.2 + sinon: + specifier: 18.0.1 + version: 18.0.1 + storybook: + specifier: 10.3.3 + version: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwindcss: + specifier: 4.2.1 + version: 4.2.1 + tw-animate-css: + specifier: 1.4.0 + version: 1.4.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/signup-form: + dependencies: + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@storybook/addon-essentials': + specifier: 8.6.14 + version: 8.6.14(@types/react@18.3.28)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-interactions': + specifier: 8.6.14 + version: 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-links': + specifier: 8.6.14 + version: 8.6.14(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-styling': + specifier: 1.3.7 + version: 1.3.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(encoding@0.1.13)(less@4.6.4)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)) + '@storybook/blocks': + specifier: 8.6.14 + version: 8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/react': + specifier: 8.6.14 + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3) + '@storybook/react-vite': + specifier: 8.6.14 + version: 8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.0)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@storybook/testing-library': + specifier: 0.2.2 + version: 0.2.2 + '@tailwindcss/line-clamp': + specifier: 0.4.4 + version: 0.4.4(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) + '@tryghost/i18n': + specifier: workspace:* + version: link:../../ghost/i18n + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.6) + concurrently: + specifier: 8.2.2 + version: 8.2.2 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: 4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: 0.4.24 + version: 0.4.24(eslint@8.57.1) + eslint-plugin-tailwindcss: + specifier: 3.18.2 + version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)) + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + postcss: + specifier: 8.5.6 + version: 8.5.6 + postcss-import: + specifier: 16.1.1 + version: 16.1.1(postcss@8.5.6) + prop-types: + specifier: 15.8.1 + version: 15.8.1 + rollup-plugin-node-builtins: + specifier: 2.1.2 + version: 2.1.2 + storybook: + specifier: 8.6.15 + version: 8.6.15(prettier@2.8.8) + stylelint: + specifier: 15.11.0 + version: 15.11.0(typescript@5.9.3) + tailwindcss: + specifier: 3.4.18 + version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + apps/sodo-search: + dependencies: + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + '@tryghost/i18n': + specifier: workspace:* + version: link:../../ghost/i18n + flexsearch: + specifier: 0.8.153 + version: 0.8.153 + react: + specifier: 17.0.2 + version: 17.0.2 + react-dom: + specifier: 17.0.2 + version: 17.0.2(react@17.0.2) + devDependencies: + '@testing-library/jest-dom': + specifier: 5.17.0 + version: 5.17.0 + '@testing-library/react': + specifier: 12.1.5 + version: 12.1.5(@types/react@18.3.28)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: ~3.2.4 + version: 3.2.4(vitest@3.2.4) + cross-fetch: + specifier: 4.1.0 + version: 4.1.0(encoding@0.1.13) + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + nock: + specifier: 13.5.6 + version: 13.5.6 + tailwindcss: + specifier: 3.4.18 + version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 3.3.0 + version: 3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 3.2.4 + version: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + apps/stats: + dependencies: + '@svg-maps/world': + specifier: 1.0.1 + version: 1.0.1 + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../admin-x-framework + '@tryghost/shade': + specifier: workspace:* + version: link:../shade + i18n-iso-countries: + specifier: 7.14.0 + version: 7.14.0 + moment: + specifier: 2.24.0 + version: 2.24.0 + moment-timezone: + specifier: 0.5.45 + version: 0.5.45 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + react-svg-map: + specifier: 2.2.0 + version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + devDependencies: + '@faker-js/faker': + specifier: 9.9.0 + version: 9.9.0 + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@tanstack/react-query': + specifier: 4.36.1 + version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 14.3.1 + version: 14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jest': + specifier: 29.5.14 + version: 29.5.14 + '@types/react': + specifier: 18.3.28 + version: 18.3.28 + '@types/react-svg-map': + specifier: 2.1.4 + version: 2.1.4 + '@vitejs/plugin-react': + specifier: 4.7.0 + version: 4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@vitest/coverage-v8': + specifier: ^1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + dotenv: + specifier: 17.3.1 + version: 17.3.1 + msw: + specifier: 2.12.14 + version: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + vite: + specifier: 5.4.21 + version: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-plugin-svgr: + specifier: 4.5.0 + version: 4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + vitest: + specifier: 1.6.1 + version: 1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + e2e: + devDependencies: + '@eslint/js': + specifier: 'catalog:' + version: 8.57.1 + '@faker-js/faker': + specifier: 8.4.1 + version: 8.4.1 + '@playwright/test': + specifier: 1.59.1 + version: 1.59.1 + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + '@tryghost/logging': + specifier: 2.5.5 + version: 2.5.5 + '@types/dockerode': + specifier: 3.3.47 + version: 3.3.47 + '@types/express': + specifier: 4.17.25 + version: 4.17.25 + busboy: + specifier: ^1.6.0 + version: 1.6.0 + c8: + specifier: 10.1.3 + version: 10.1.3 + dockerode: + specifier: 4.0.10 + version: 4.0.10 + dotenv: + specifier: 17.3.1 + version: 17.3.1 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-no-relative-import-paths: + specifier: 1.6.1 + version: 1.6.1 + eslint-plugin-playwright: + specifier: 2.10.1 + version: 2.10.1(eslint@8.57.1) + express: + specifier: 4.21.2 + version: 4.21.2 + knex: + specifier: 3.1.0 + version: 3.1.0(mysql2@3.18.1(@types/node@25.6.0)) + mysql2: + specifier: 3.18.1 + version: 3.18.1(@types/node@25.6.0) + stripe: + specifier: 8.222.0 + version: 8.222.0 + ts-node: + specifier: 10.9.2 + version: 10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3) + typescript: + specifier: 5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: 8.58.0 + version: 8.58.0(eslint@8.57.1)(typescript@5.9.3) + + ghost/admin: + dependencies: + i18n-iso-countries: + specifier: 7.14.0 + version: 7.14.0 + lru-cache: + specifier: 6.0.0 + version: 6.0.0 + path-browserify: + specifier: 1.0.1 + version: 1.0.1 + webpack: + specifier: 5.105.4 + version: 5.105.4(@swc/core@1.15.21) + devDependencies: + '@babel/eslint-parser': + specifier: 7.28.4 + version: 7.28.4(@babel/core@7.29.0)(eslint@8.57.1) + '@babel/plugin-proposal-class-properties': + specifier: 7.18.6 + version: 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': + specifier: 7.28.0 + version: 7.28.0(@babel/core@7.29.0) + '@ember/jquery': + specifier: 2.0.0 + version: 2.0.0 + '@ember/optional-features': + specifier: 2.1.0 + version: 2.1.0 + '@ember/render-modifiers': + specifier: 2.1.0 + version: 2.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + '@ember/test-helpers': + specifier: 2.9.6 + version: 2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + '@ember/test-waiters': + specifier: 3.1.0 + version: 3.1.0 + '@embroider/macros': + specifier: 1.16.13 + version: 1.16.13 + '@faker-js/faker': + specifier: 7.6.0 + version: 7.6.0 + '@glimmer/component': + specifier: 1.1.2 + version: 1.1.2(@babel/core@7.29.0) + '@html-next/vertical-collection': + specifier: 3.0.0 + version: 3.0.0(@babel/core@7.29.0) + '@sentry/ember': + specifier: 7.120.3 + version: 7.120.3(webpack@5.105.4(@swc/core@1.15.21)) + '@sentry/integrations': + specifier: 7.114.0 + version: 7.114.0 + '@sentry/replay': + specifier: 7.116.0 + version: 7.116.0 + '@tryghost/admin-x-framework': + specifier: workspace:* + version: link:../../apps/admin-x-framework + '@tryghost/color-utils': + specifier: 0.2.16 + version: 0.2.16 + '@tryghost/ember-promise-modals': + specifier: 2.0.1 + version: 2.0.1(ember-source@3.24.0(@babel/core@7.29.0))(postcss@8.5.6) + '@tryghost/helpers': + specifier: 1.1.103 + version: 1.1.103 + '@tryghost/kg-clean-basic-html': + specifier: 4.2.23 + version: 4.2.23 + '@tryghost/kg-converters': + specifier: 1.1.21 + version: 1.1.21 + '@tryghost/koenig-lexical': + specifier: 1.7.30 + version: 1.7.30 + '@tryghost/limit-service': + specifier: 1.5.2 + version: 1.5.2 + '@tryghost/members-csv': + specifier: 2.0.5 + version: 2.0.5 + '@tryghost/nql': + specifier: 0.12.10 + version: 0.12.10 + '@tryghost/nql-lang': + specifier: 0.6.4 + version: 0.6.4 + '@tryghost/string': + specifier: 0.3.2 + version: 0.3.2 + '@tryghost/timezone-data': + specifier: 0.4.18 + version: 0.4.18 + animejs: + specifier: 3.2.2 + version: 3.2.2 + autoprefixer: + specifier: 9.8.6 + version: 9.8.6 + babel-plugin-transform-class-properties: + specifier: 6.24.1 + version: 6.24.1 + babel-plugin-transform-react-jsx: + specifier: 6.24.1 + version: 6.24.1 + broccoli-asset-rev: + specifier: 3.0.0 + version: 3.0.0 + broccoli-concat: + specifier: 4.2.7 + version: 4.2.7 + broccoli-funnel: + specifier: 3.0.8 + version: 3.0.8 + broccoli-merge-trees: + specifier: 4.2.0 + version: 4.2.0 + broccoli-terser-sourcemap: + specifier: 4.1.1 + version: 4.1.1 + chai: + specifier: 4.5.0 + version: 4.5.0 + chai-dom: + specifier: 1.12.1 + version: 1.12.1(chai@4.5.0) + codemirror: + specifier: 5.48.2 + version: 5.48.2 + cssnano: + specifier: 4.1.10 + version: 4.1.10 + element-resize-detector: + specifier: 1.2.4 + version: 1.2.4 + ember-ajax: + specifier: 5.1.2 + version: 5.1.2 + ember-assign-helper: + specifier: 0.5.0 + version: 0.5.0(ember-source@3.24.0(@babel/core@7.29.0)) + ember-auto-import: + specifier: 2.10.0 + version: 2.10.0(webpack@5.105.4(@swc/core@1.15.21)) + ember-classic-decorator: + specifier: 3.0.1 + version: 3.0.1 + ember-cli: + specifier: 3.24.0 + version: 3.24.0(encoding@0.1.13)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + ember-cli-app-version: + specifier: 5.0.0 + version: 5.0.0 + ember-cli-babel: + specifier: 8.2.0 + version: 8.2.0(@babel/core@7.29.0) + ember-cli-chart: + specifier: 3.7.2 + version: 3.7.2 + ember-cli-code-coverage: + specifier: 1.0.3 + version: 1.0.3 + ember-cli-dependency-checker: + specifier: 3.3.2 + version: 3.3.2(ember-cli@3.24.0(encoding@0.1.13)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8)) + ember-cli-deprecation-workflow: + specifier: 2.2.0 + version: 2.2.0 + ember-cli-htmlbars: + specifier: 6.3.0 + version: 6.3.0 + ember-cli-inject-live-reload: + specifier: 2.1.0 + version: 2.1.0 + ember-cli-mirage: + specifier: 2.4.0 + version: 2.4.0(@babel/core@7.29.0)(@ember/test-helpers@2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)))(ember-data@3.24.0(@babel/core@7.29.0))(ember-source@3.24.0(@babel/core@7.29.0)) + ember-cli-node-assets: + specifier: 0.2.2 + version: 0.2.2 + ember-cli-postcss: + specifier: 6.0.1 + version: 6.0.1 + ember-cli-shims: + specifier: 1.2.0 + version: 1.2.0 + ember-cli-string-helpers: + specifier: 6.1.0 + version: 6.1.0 + ember-cli-terser: + specifier: 4.0.1 + version: 4.0.1 + ember-cli-test-loader: + specifier: 3.1.0 + version: 3.1.0 + ember-composable-helpers: + specifier: 5.0.0 + version: 5.0.0 + ember-concurrency: + specifier: 2.3.7 + version: 2.3.7(@babel/core@7.29.0) + ember-could-get-used-to-this: + specifier: 1.0.1 + version: 1.0.1 + ember-css-transitions: + specifier: 4.4.1 + version: 4.4.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-data: + specifier: 3.24.0 + version: 3.24.0(@babel/core@7.29.0) + ember-decorators: + specifier: 6.1.1 + version: 6.1.1 + ember-drag-drop: + specifier: 0.4.8 + version: 0.4.8(@babel/core@7.29.0) + ember-ella-sparse: + specifier: 0.16.0 + version: 0.16.0(@babel/core@7.29.0) + ember-exam: + specifier: 6.0.1 + version: 6.0.1(ember-mocha@0.16.2(@babel/core@7.29.0)) + ember-export-application-global: + specifier: 2.0.1 + version: 2.0.1 + ember-fetch: + specifier: 8.1.2 + version: 8.1.2(encoding@0.1.13) + ember-in-viewport: + specifier: 4.1.0 + version: 4.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(webpack@5.105.4(@swc/core@1.15.21)) + ember-infinity: + specifier: 2.3.0 + version: 2.3.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(webpack@5.105.4(@swc/core@1.15.21)) + ember-keyboard: + specifier: 8.2.1 + version: 8.2.1(@babel/core@7.29.0)(@ember/test-helpers@2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)))(ember-source@3.24.0(@babel/core@7.29.0)) + ember-load: + specifier: 0.0.17 + version: 0.0.17 + ember-load-initializers: + specifier: 2.1.2 + version: 2.1.2(@babel/core@7.29.0) + ember-mocha: + specifier: 0.16.2 + version: 0.16.2(@babel/core@7.29.0) + ember-modifier: + specifier: 4.2.0 + version: 4.2.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-moment: + specifier: 10.0.1 + version: 10.0.1(moment-timezone@0.5.45)(moment@2.24.0) + ember-one-way-select: + specifier: 4.0.1 + version: 4.0.1 + ember-power-datepicker: + specifier: 0.8.1 + version: 0.8.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-power-select: + specifier: 6.0.1 + version: 6.0.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-resolver: + specifier: 8.1.0 + version: 8.1.0(@babel/core@7.29.0) + ember-simple-auth: + specifier: 5.0.0 + version: 5.0.0(ember-fetch@8.1.2(encoding@0.1.13)) + ember-sinon: + specifier: 5.0.0 + version: 5.0.0 + ember-source: + specifier: 3.24.0 + version: 3.24.0(@babel/core@7.29.0) + ember-svg-jar: + specifier: 2.7.1 + version: 2.7.1 + ember-template-lint: + specifier: 5.13.0 + version: 5.13.0 + ember-test-selectors: + specifier: 6.0.0 + version: 6.0.0 + ember-tooltips: + specifier: 3.6.0 + version: 3.6.0 + ember-truth-helpers: + specifier: 3.1.1 + version: 3.1.1 + eslint: + specifier: 'catalog:' + version: 8.57.1 + eslint-plugin-babel: + specifier: 5.3.1 + version: 5.3.1(eslint@8.57.1) + flexsearch: + specifier: 0.7.43 + version: 0.7.43 + fs-extra: + specifier: 11.3.4 + version: 11.3.4 + ghost: + specifier: workspace:* + version: link:../core + glob: + specifier: 8.1.0 + version: 8.1.0 + google-caja-bower: + specifier: https://github.com/acburdine/google-caja-bower#ghost + version: https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd + keymaster: + specifier: https://github.com/madrobby/keymaster.git + version: https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49 + liquid-fire: + specifier: 0.34.0 + version: 0.34.0(ember-source@3.24.0(@babel/core@7.29.0)) + liquid-wormhole: + specifier: 3.0.1 + version: 3.0.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(liquid-fire@0.34.0(ember-source@3.24.0(@babel/core@7.29.0))) + loader.js: + specifier: 4.7.0 + version: 4.7.0 + microdiff: + specifier: 1.5.0 + version: 1.5.0 + miragejs: + specifier: 0.1.48 + version: 0.1.48 + moment-timezone: + specifier: 0.5.45 + version: 0.5.45 + normalize.css: + specifier: 3.0.3 + version: 3.0.3 + papaparse: + specifier: 5.5.3 + version: 5.5.3 + postcss-color-mod-function: + specifier: 3.0.3 + version: 3.0.3 + postcss-custom-media: + specifier: 7.0.8 + version: 7.0.8 + postcss-custom-properties: + specifier: 10.0.0 + version: 10.0.0 + postcss-import: + specifier: 12.0.1 + version: 12.0.1 + pretender: + specifier: 3.4.7 + version: 3.4.7 + process: + specifier: 0.11.10 + version: 0.11.10 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) + reframe.js: + specifier: 4.0.2 + version: 4.0.2 + semver: + specifier: 7.7.4 + version: 7.7.4 + sentry-testkit: + specifier: 5.0.10 + version: 5.0.10 + sinon-chai: + specifier: 4.0.1 + version: 4.0.1(chai@4.5.0)(sinon@21.1.1) + testem: + specifier: 3.19.1 + version: 3.19.1(@babel/core@7.29.0)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + tracked-built-ins: + specifier: 3.4.0 + version: 3.4.0(@babel/core@7.29.0) + util: + specifier: 0.12.5 + version: 0.12.5 + validator: + specifier: 13.12.0 + version: 13.12.0 + walk-sync: + specifier: 3.0.0 + version: 3.0.0 + + ghost/core: + dependencies: + '@aws-sdk/client-s3': + specifier: 3.1025.0 + version: 3.1025.0 + '@extractus/oembed-extractor': + specifier: 3.2.1 + version: 3.2.1(encoding@0.1.13) + '@faker-js/faker': + specifier: 7.6.0 + version: 7.6.0 + '@isaacs/ttlcache': + specifier: 1.4.1 + version: 1.4.1 + '@sentry/node': + specifier: 7.120.4 + version: 7.120.4 + '@slack/webhook': + specifier: 7.0.9 + version: 7.0.9 + '@tryghost/adapter-base-cache': + specifier: 0.1.23 + version: 0.1.23 + '@tryghost/admin-api-schema': + specifier: 4.7.2 + version: 4.7.2 + '@tryghost/api-framework': + specifier: 1.0.7 + version: 1.0.7 + '@tryghost/bookshelf-plugins': + specifier: 2.0.3 + version: 2.0.3 + '@tryghost/color-utils': + specifier: 0.2.16 + version: 0.2.16 + '@tryghost/config-url-helpers': + specifier: 1.0.23 + version: 1.0.23 + '@tryghost/custom-fonts': + specifier: 1.0.8 + version: 1.0.8 + '@tryghost/database-info': + specifier: 0.3.35 + version: 0.3.35 + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + '@tryghost/domain-events': + specifier: 1.0.8 + version: 1.0.8 + '@tryghost/email-mock-receiver': + specifier: 0.3.16 + version: 0.3.16 + '@tryghost/errors': + specifier: ^1.3.7 + version: 1.3.13 + '@tryghost/helpers': + specifier: 1.1.103 + version: 1.1.103 + '@tryghost/html-to-plaintext': + specifier: 1.0.8 + version: 1.0.8 + '@tryghost/http-cache-utils': + specifier: 0.1.25 + version: 0.1.25 + '@tryghost/i18n': + specifier: workspace:* + version: link:../i18n + '@tryghost/image-transform': + specifier: 1.4.13 + version: 1.4.13 + '@tryghost/job-manager': + specifier: 1.0.9 + version: 1.0.9 + '@tryghost/kg-card-factory': + specifier: 5.1.14 + version: 5.1.14 + '@tryghost/kg-clean-basic-html': + specifier: 4.2.23 + version: 4.2.23 + '@tryghost/kg-converters': + specifier: 1.1.21 + version: 1.1.21 + '@tryghost/kg-default-atoms': + specifier: 5.1.9 + version: 5.1.9 + '@tryghost/kg-default-cards': + specifier: 10.2.13 + version: 10.2.13(encoding@0.1.13) + '@tryghost/kg-default-nodes': + specifier: 2.0.21 + version: 2.0.21(@noble/hashes@1.8.0) + '@tryghost/kg-default-transforms': + specifier: 1.2.44 + version: 1.2.44(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + '@tryghost/kg-html-to-lexical': + specifier: 1.2.45 + version: 1.2.45(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + '@tryghost/kg-lexical-html-renderer': + specifier: 1.3.44 + version: 1.3.44(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + '@tryghost/kg-markdown-html-renderer': + specifier: 7.1.18 + version: 7.1.18 + '@tryghost/kg-mobiledoc-html-renderer': + specifier: 7.1.18 + version: 7.1.18 + '@tryghost/limit-service': + specifier: 1.5.2 + version: 1.5.2 + '@tryghost/logging': + specifier: 2.5.5 + version: 2.5.5 + '@tryghost/members-csv': + specifier: 2.0.5 + version: 2.0.5 + '@tryghost/metrics': + specifier: 1.0.43 + version: 1.0.43 + '@tryghost/mongo-utils': + specifier: 0.6.3 + version: 0.6.3 + '@tryghost/mw-error-handler': + specifier: 1.0.13 + version: 1.0.13 + '@tryghost/mw-vhost': + specifier: 1.0.6 + version: 1.0.6 + '@tryghost/nodemailer': + specifier: 0.3.48 + version: 0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + '@tryghost/nql': + specifier: 0.12.10 + version: 0.12.10 + '@tryghost/nql-lang': + specifier: 0.6.4 + version: 0.6.4 + '@tryghost/parse-email-address': + specifier: workspace:* + version: link:../parse-email-address + '@tryghost/pretty-cli': + specifier: 1.2.52 + version: 1.2.52 + '@tryghost/prometheus-metrics': + specifier: 1.0.8 + version: 1.0.8 + '@tryghost/promise': + specifier: 0.3.20 + version: 0.3.20 + '@tryghost/referrer-parser': + specifier: 0.1.15 + version: 0.1.15 + '@tryghost/request': + specifier: 1.0.12 + version: 1.0.12 + '@tryghost/root-utils': + specifier: 0.3.38 + version: 0.3.38 + '@tryghost/security': + specifier: 1.0.6 + version: 1.0.6 + '@tryghost/social-urls': + specifier: 0.1.60 + version: 0.1.60 + '@tryghost/string': + specifier: 0.3.2 + version: 0.3.2 + '@tryghost/tpl': + specifier: 0.1.40 + version: 0.1.40 + '@tryghost/url-utils': + specifier: 5.1.2 + version: 5.1.2 + '@tryghost/validator': + specifier: 0.2.22 + version: 0.2.22 + '@tryghost/version': + specifier: 0.1.38 + version: 0.1.38 + '@tryghost/zip': + specifier: 1.1.54 + version: 1.1.54 + body-parser: + specifier: 1.20.4 + version: 1.20.4 + bookshelf: + specifier: 1.2.0 + version: 1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)) + bookshelf-relations: + specifier: 2.8.0 + version: 2.8.0(bookshelf@1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7))) + brute-knex: + specifier: 4.0.1 + version: 4.0.1(express@4.21.2)(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + bson-objectid: + specifier: 2.0.4 + version: 2.0.4 + cache-manager: + specifier: 4.1.0 + version: 4.1.0 + cache-manager-ioredis: + specifier: 2.1.0 + version: 2.1.0 + chalk: + specifier: 4.1.2 + version: 4.1.2 + charset: + specifier: 1.0.1 + version: 1.0.1 + cheerio: + specifier: 0.22.0 + version: 0.22.0 + clsx: + specifier: 2.1.1 + version: 2.1.1 + cluster-key-slot: + specifier: 1.1.2 + version: 1.1.2 + common-tags: + specifier: 1.8.2 + version: 1.8.2 + compression: + specifier: 1.8.1 + version: 1.8.1 + connect-slashes: + specifier: 1.4.0 + version: 1.4.0 + cookie-session: + specifier: 2.1.1 + version: 2.1.1 + cookies: + specifier: 0.9.1 + version: 0.9.1 + cors: + specifier: 2.8.6 + version: 2.8.6 + countries-and-timezones: + specifier: 3.8.0 + version: 3.8.0 + csso: + specifier: 5.0.5 + version: 5.0.5 + csv-writer: + specifier: 1.6.0 + version: 1.6.0 + date-fns: + specifier: 2.30.0 + version: 2.30.0 + dompurify: + specifier: 3.3.0 + version: 3.3.0 + downsize: + specifier: 0.0.8 + version: 0.0.8 + entities: + specifier: 4.5.0 + version: 4.5.0 + express: + specifier: 4.21.2 + version: 4.21.2 + express-brute: + specifier: 1.0.1 + version: 1.0.1(express@4.21.2) + express-hbs: + specifier: 2.5.0 + version: 2.5.0 + express-jwt: + specifier: 8.5.1 + version: 8.5.1 + express-lazy-router: + specifier: 1.0.6 + version: 1.0.6(express@4.21.2) + express-query-boolean: + specifier: 2.0.0 + version: 2.0.0 + express-queue: + specifier: 0.0.13 + version: 0.0.13 + express-session: + specifier: 1.19.0 + version: 1.19.0 + file-type: + specifier: 16.5.4 + version: 16.5.4 + form-data: + specifier: 4.0.5 + version: 4.0.5 + fs-extra: + specifier: 11.3.4 + version: 11.3.4 + ghost-storage-base: + specifier: 1.1.2 + version: 1.1.2 + glob: + specifier: 8.1.0 + version: 8.1.0 + got: + specifier: 13.0.0 + version: 13.0.0 + gscan: + specifier: 5.4.3 + version: 5.4.3 + handlebars: + specifier: 4.7.9 + version: 4.7.9 + heic-convert: + specifier: 2.1.0 + version: 2.1.0 + html-to-text: + specifier: 5.1.1 + version: 5.1.1 + html5parser: + specifier: 2.0.2 + version: 2.0.2 + human-number: + specifier: 2.0.10 + version: 2.0.10 + iconv-lite: + specifier: 0.7.2 + version: 0.7.2 + image-size: + specifier: 1.2.1 + version: 1.2.1 + intl: + specifier: 1.2.5 + version: 1.2.5 + intl-messageformat: + specifier: 5.4.3 + version: 5.4.3 + js-yaml: + specifier: 4.1.1 + version: 4.1.1 + jsdom: + specifier: 28.1.0 + version: 28.1.0(@noble/hashes@1.8.0) + jsonc-parser: + specifier: 3.3.1 + version: 3.3.1 + jsonwebtoken: + specifier: 8.5.1 + version: 8.5.1 + juice: + specifier: 9.1.0 + version: 9.1.0(encoding@0.1.13) + keypair: + specifier: 1.0.4 + version: 1.0.4 + knex: + specifier: 2.4.2 + version: 2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + knex-migrator: + specifier: 5.3.2 + version: 5.3.2 + leaky-bucket: + specifier: 2.2.0 + version: 2.2.0 + lodash: + specifier: 4.17.23 + version: 4.17.23 + luxon: + specifier: 3.7.2 + version: 3.7.2 + mailgun.js: + specifier: 10.4.0 + version: 10.4.0 + metascraper: + specifier: 5.45.15 + version: 5.45.15(@noble/hashes@1.8.0) + metascraper-author: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-description: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-image: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-logo: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-logo-favicon: + specifier: 5.42.0 + version: 5.42.0(@noble/hashes@1.8.0) + metascraper-publisher: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-title: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + metascraper-url: + specifier: 5.45.10 + version: 5.45.10(@noble/hashes@1.8.0) + mime-types: + specifier: 2.1.35 + version: 2.1.35 + mingo: + specifier: 2.5.3 + version: 2.5.3 + moment: + specifier: 2.24.0 + version: 2.24.0 + moment-timezone: + specifier: 0.5.45 + version: 0.5.45 + multer: + specifier: 2.0.2 + version: 2.0.2 + mysql2: + specifier: 3.18.1 + version: 3.18.1(@types/node@22.19.17) + nconf: + specifier: 0.13.0 + version: 0.13.0 + node-fetch: + specifier: 2.7.0 + version: 2.7.0(encoding@0.1.13) + node-jose: + specifier: 2.2.0 + version: 2.2.0 + nodemailer: + specifier: 6.10.1 + version: 6.10.1 + on-headers: + specifier: ^1.1.0 + version: 1.1.0 + otplib: + specifier: 12.0.1 + version: 12.0.1 + papaparse: + specifier: 5.5.3 + version: 5.5.3 + path-match: + specifier: 1.2.4 + version: 1.2.4 + probability-distributions: + specifier: 0.9.1 + version: 0.9.1 + probe-image-size: + specifier: 7.2.3 + version: 7.2.3 + rss: + specifier: 1.2.2 + version: 1.2.2 + sanitize-html: + specifier: 2.17.0 + version: 2.17.0 + semver: + specifier: 7.7.4 + version: 7.7.4 + simple-dom: + specifier: 1.4.0 + version: 1.4.0 + stoppable: + specifier: 1.1.0 + version: 1.1.0 + stripe: + specifier: 8.222.0 + version: 8.222.0 + superagent: + specifier: 5.3.1 + version: 5.3.1 + superagent-throttle: + specifier: 1.0.1 + version: 1.0.1 + terser: + specifier: 5.46.1 + version: 5.46.1 + tiny-glob: + specifier: 0.2.9 + version: 0.2.9 + ua-parser-js: + specifier: 1.0.41 + version: 1.0.41 + xml: + specifier: 1.0.1 + version: 1.0.1 + devDependencies: + '@actions/core': + specifier: 3.0.0 + version: 3.0.0 + '@prettier/sync': + specifier: 0.6.1 + version: 0.6.1(prettier@2.8.8) + '@tryghost/express-test': + specifier: 0.15.5 + version: 0.15.5 + '@tryghost/webhook-mock-receiver': + specifier: 0.2.22 + version: 0.2.22 + '@types/bookshelf': + specifier: 1.2.9 + version: 1.2.9(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + '@types/common-tags': + specifier: 1.8.4 + version: 1.8.4 + '@types/express': + specifier: 4.17.25 + version: 4.17.25 + '@types/jsdom': + specifier: 28.0.1 + version: 28.0.1 + '@types/jsonwebtoken': + specifier: 9.0.10 + version: 9.0.10 + '@types/lodash': + specifier: 4.17.24 + version: 4.17.24 + '@types/lodash-es': + specifier: 4.17.12 + version: 4.17.12 + '@types/mime-types': + specifier: 3.0.1 + version: 3.0.1 + '@types/mocha': + specifier: 10.0.10 + version: 10.0.10 + '@types/node': + specifier: 22.19.17 + version: 22.19.17 + '@types/node-fetch': + specifier: 2.6.13 + version: 2.6.13 + '@types/node-jose': + specifier: 1.1.13 + version: 1.1.13 + '@types/nodemailer': + specifier: 6.4.23 + version: 6.4.23 + '@types/on-headers': + specifier: 1.0.4 + version: 1.0.4 + '@types/sinon': + specifier: 17.0.4 + version: 17.0.4 + '@types/supertest': + specifier: 6.0.3 + version: 6.0.3 + c8: + specifier: 10.1.3 + version: 10.1.3 + cli-progress: + specifier: 3.12.0 + version: 3.12.0 + cssnano: + specifier: 7.1.1 + version: 7.1.1(postcss@8.5.6) + detect-indent: + specifier: 6.1.0 + version: 6.1.0 + detect-newline: + specifier: 3.1.0 + version: 3.1.0 + expect: + specifier: 29.7.0 + version: 29.7.0 + find-root: + specifier: 1.1.0 + version: 1.1.0 + html-minifier: + specifier: 4.0.0 + version: 4.0.0 + html-validate: + specifier: 8.29.0 + version: 8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)))(vitest@1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + inquirer: + specifier: 8.2.7 + version: 8.2.7(@types/node@22.19.17) + jwk-to-pem: + specifier: 2.0.7 + version: 2.0.7 + jwks-rsa: + specifier: 3.2.0 + version: 3.2.0 + lodash-es: + specifier: 4.17.23 + version: 4.17.23 + mocha: + specifier: 11.7.5 + version: 11.7.5 + mocha-slow-test-reporter: + specifier: 0.1.2 + version: 0.1.2 + mock-knex: + specifier: TryGhost/mock-knex#68948e11b0ea4fe63456098dfdc169bea7f62009 + version: https://codeload.github.com/TryGhost/mock-knex/tar.gz/68948e11b0ea4fe63456098dfdc169bea7f62009(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)) + nock: + specifier: 13.5.6 + version: 13.5.6 + nodemon: + specifier: 3.1.14 + version: 3.1.14 + parse-prometheus-text-format: + specifier: 1.1.1 + version: 1.1.1 + postcss: + specifier: 8.5.6 + version: 8.5.6 + postcss-cli: + specifier: 11.0.1 + version: 11.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0) + rewire: + specifier: 9.0.1 + version: 9.0.1(jiti@2.6.1) + sinon: + specifier: 18.0.1 + version: 18.0.1 + supertest: + specifier: 6.3.4 + version: 6.3.4 + tmp: + specifier: 0.2.5 + version: 0.2.5 + toml: + specifier: 3.0.0 + version: 3.0.0 + tsx: + specifier: 4.21.0 + version: 4.21.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + optionalDependencies: + '@tryghost/html-to-mobiledoc': + specifier: 3.2.26 + version: 3.2.26(@noble/hashes@1.8.0) + sqlite3: + specifier: 5.1.7 + version: 5.1.7 + + ghost/i18n: + dependencies: + '@tryghost/debug': + specifier: 0.1.40 + version: 0.1.40 + i18next: + specifier: 23.16.8 + version: 23.16.8 + devDependencies: + c8: + specifier: 10.1.3 + version: 10.1.3 + glob: + specifier: ^13.0.6 + version: 13.0.6 + i18next-parser: + specifier: 8.13.0 + version: 8.13.0 + mocha: + specifier: 11.7.5 + version: 11.7.5 + + ghost/parse-email-address: + dependencies: + parse-email-address: + specifier: 0.0.2 + version: 0.0.2 + devDependencies: + '@types/node': + specifier: 25.6.0 + version: 25.6.0 + c8: + specifier: 10.1.3 + version: 10.1.3 + mocha: + specifier: 11.7.5 + version: 11.7.5 + sinon: + specifier: 21.0.1 + version: 21.0.1 + ts-node: + specifier: 10.9.2 + version: 10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3) + tsx: + specifier: 4.21.0 + version: 4.21.0 + typescript: + specifier: 5.9.3 + version: 5.9.3 + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@actions/core@3.0.0': + resolution: {integrity: sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==} + + '@actions/exec@3.0.0': + resolution: {integrity: sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==} + + '@actions/http-client@4.0.0': + resolution: {integrity: sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==} + + '@actions/io@3.0.2': + resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==} + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/dom-selector@7.0.4': + resolution: {integrity: sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1025.0': + resolution: {integrity: sha512-9Byz2fPnuGRRL8DTTD5bYPl1Iwm+ysLiCMgptffa3lNkVLCiUZc5e5TAaOjk0MvyeXieq+jn35AmQL6cgN2KHQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-ses@3.1018.0': + resolution: {integrity: sha512-OzNZoivE8D0jP7hYrXkiwCjDH8aOoX7xviQNi7CPuis1ua4uNllaYlwdtIl2JOU6OUJ0N4Ml0yNYGBJH1T+mEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.25': + resolution: {integrity: sha512-TNrx7eq6nKNOO62HWPqoBqPLXEkW6nLZQGwjL6lq1jZtigWYbK1NbCnT7mKDzbLMHZfuOECUt3n6CzxjUW9HWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.26': + resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.5': + resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.23': + resolution: {integrity: sha512-EamaclJcCEaPHp6wiVknNMM2RlsPMjAHSsYSFLNENBM8Wz92QPc6cOn3dif6vPDQt0Oo4IEghDy3NMDCzY/IvA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.24': + resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.25': + resolution: {integrity: sha512-qPymamdPcLp6ugoVocG1y5r69ScNiRzb0hogX25/ij+Wz7c7WnsgjLTaz7+eB5BfRxeyUwuw5hgULMuwOGOpcw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.26': + resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.25': + resolution: {integrity: sha512-G/v/PicYn4qs7xCv4vT6I4QKdvMyRvsgIFNBkUueCGlbLo7/PuKcNKgUozmLSsaYnE7jIl6UrfkP07EUubr48w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.28': + resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.25': + resolution: {integrity: sha512-bUdmyJeVua7SmD+g2a65x2/0YqsGn4K2k4GawI43js0odaNaIzpIhLtHehUnPnfLuyhPWbJR1NyuIO4iMVfM0w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.28': + resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.26': + resolution: {integrity: sha512-5XSK74rCXxCNj+UWv5bjq1EccYkiyW4XOHFU9NXnsCcQF8dJuHdua1qFg0m/LIwVOWklbKsrcnMtfxIXwgvwzQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.29': + resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.23': + resolution: {integrity: sha512-IL/TFW59++b7MpHserjUblGrdP5UXy5Ekqqx1XQkERXBFJcZr74I7VaSrQT5dxdRMU16xGK4L0RQ5fQG1pMgnA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.24': + resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.25': + resolution: {integrity: sha512-r4OGAfHmlEa1QBInHWz+/dOD4tRljcjVNQe9wJ/AJNXEj1d2WdsRLppvRFImRV6FIs+bTpjtL0a23V5ELQpRPw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.28': + resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.25': + resolution: {integrity: sha512-uM1OtoJgj+yK3MlAmda8uR9WJJCdm5HB25JyCeFL5a5q1Fbafalf4uKidFO3/L0Pgd+Fsflkb4cM6jHIswi3QQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.28': + resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.8': + resolution: {integrity: sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.8': + resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.6': + resolution: {integrity: sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.8': + resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.8': + resolution: {integrity: sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.8': + resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.9': + resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.27': + resolution: {integrity: sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.8': + resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.26': + resolution: {integrity: sha512-AilFIh4rI/2hKyyGN6XrB0yN96W2o7e7wyrPWCM6QjZM1mcC/pVkW3IWWRvuBWMpVP8Fg+rMpbzeLQ6dTM4gig==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.28': + resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.15': + resolution: {integrity: sha512-k6WAVNkub5DrU46iPQvH1m0xc1n+0dX79+i287tYJzf5g1yU2rX3uf4xNeL5JvK1NtYgfwMnsxHqhOXFBn367A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.18': + resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.10': + resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.15': + resolution: {integrity: sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1018.0': + resolution: {integrity: sha512-97OPNJHy37wmGOX44xAcu6E9oSTiqK9uPcy/fWpmN5uB3JuEp1f6x60Xot/jp+FxwhQWIFUsVJFnm3QKqt7T6Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1021.0': + resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.6': + resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.5': + resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.8': + resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} + + '@aws-sdk/util-user-agent-node@3.973.12': + resolution: {integrity: sha512-8phW0TS8ntENJgDcFewYT/Q8dOmarpvSxEjATu2GUBAutiHr++oEGCiBUwxslCMNvwW2cAPZNT53S/ym8zm/gg==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/util-user-agent-node@3.973.14': + resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.16': + resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/eslint-parser@7.28.4': + resolution: {integrity: sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-optional-chaining@7.21.0': + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-methods@7.18.6': + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.11': + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.0': + resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-assign@7.27.1': + resolution: {integrity: sha512-LP6tsnirA6iy13uBKiYgjJsfQrodmlSrpZModtlo1Vk8sOO68gfo7dfA9TGJyEgxTiO7czK4EGZm8FJEZtk4kQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.4.5': + resolution: {integrity: sha512-RPB/YeGr4ZrFKNwfuQRlMf2lxoCUaU01MTw39/OFE/RiL8HDjtn68BwEPft1P7JN4akyEmjGWAMNldOV7o9V2g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.5.5': + resolution: {integrity: sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.8.7': + resolution: {integrity: sha512-7O0UsPQVNKqpHeHLpfvOG4uXmlw+MOxYvUv6Otc9uH5SYMIxvF6eBdjkWvC3f9G+VXe0RsNExyAQBeTRug/wqQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/polyfill@7.12.1': + resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} + deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. + + '@babel/preset-env@7.29.2': + resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/runtime@7.12.18': + resolution: {integrity: sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@breejs/later@4.2.0': + resolution: {integrity: sha512-EVMD0SgJtOuFeg0lAVbCwa+qeTKILb87jqvLyUtQswGD9+ce2nB52Y5zbTF1Hc0MDFfbydcMcxb47jSdhikVHA==} + engines: {node: '>= 10'} + + '@cnakazawa/watch@1.0.4': + resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} + engines: {node: '>=0.1.95'} + hasBin: true + + '@codemirror/autocomplete@6.20.1': + resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} + + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + + '@codemirror/lint@6.9.5': + resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==} + + '@codemirror/search@6.6.0': + resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} + + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.40.0': + resolution: {integrity: sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==} + + '@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'} + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/convert-colors@1.4.0': + resolution: {integrity: sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==} + engines: {node: '>=4.0.0'} + + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@2.7.1': + resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.2': + resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@2.4.1': + resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} + engines: {node: ^14 || ^16 || >=18} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@csstools/media-query-list-parser@2.1.13': + resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + '@csstools/css-parser-algorithms': ^2.7.1 + '@csstools/css-tokenizer': ^2.4.1 + + '@csstools/postcss-cascade-layers@1.1.1': + resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-color-function@1.1.1': + resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-font-format-keywords@1.0.1': + resolution: {integrity: sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-hwb-function@1.0.2': + resolution: {integrity: sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-ic-unit@1.0.1': + resolution: {integrity: sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-is-pseudo-class@2.0.7': + resolution: {integrity: sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-nested-calc@1.0.0': + resolution: {integrity: sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-normalize-display-values@1.0.1': + resolution: {integrity: sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-oklab-function@1.1.1': + resolution: {integrity: sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-progressive-custom-properties@1.3.0': + resolution: {integrity: sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + + '@csstools/postcss-stepped-value-functions@1.0.1': + resolution: {integrity: sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-text-decoration-shorthand@1.0.0': + resolution: {integrity: sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-trigonometric-functions@1.0.2': + resolution: {integrity: sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==} + engines: {node: ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/postcss-unset-value@1.0.2': + resolution: {integrity: sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + '@csstools/selector-specificity@2.2.0': + resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.10 + + '@csstools/selector-specificity@3.1.1': + resolution: {integrity: sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.13 + + '@distributed-systems/callsite@1.1.1': + resolution: {integrity: sha512-YSA3kWjClnLmFKNpdQCZlMQoWI4N6KpR/T4MaREEQczaehcagsVorT3YDV17KR6zuJXDs7f+kkSt1o/D6SufAQ==} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@7.0.2': + resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@doist/react-interpolate@2.2.1': + resolution: {integrity: sha512-3qUQhXzAPL1YxYsNtZdBsrSyhFL1AyDHNQN8XZo2IiGZAibxCBT5QW+Ymfyf8hOKaSxb9v1fYiv3uWJfY0pwZw==} + engines: {node: ^20.0.0 || ^21.0.0 || ^22.0.0, npm: ^10.0.0 || ^11.0.0} + peerDependencies: + prop-types: 15.8.1 + react: '>=17.0.2' + react-dom: '>=17.0.2' + + '@ebay/nice-modal-react@1.2.13': + resolution: {integrity: sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA==} + peerDependencies: + react: '>16.8.0' + react-dom: '>16.8.0' + + '@elastic/elasticsearch@8.13.1': + resolution: {integrity: sha512-2G4Vu6OHw4+XTrp7AGIcOEezpPEoVrWg2JTK1v/exEKSLYquZkUdd+m4yOL3/UZ6bTj7hmXwrmYzW76BnLCkJQ==} + engines: {node: '>=18'} + + '@elastic/transport@8.4.1': + resolution: {integrity: sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==} + engines: {node: '>=16'} + + '@ember-data/adapter@3.24.0': + resolution: {integrity: sha512-vdDvvHF2QyAfLLvJPeREAVUr3M5LsjaZDJJ6Ernf8gJDZBwYvTcYGQEk39GeCzd1Qhw8F7tUTz1DGZXQQRDCrw==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/canary-features@3.24.0': + resolution: {integrity: sha512-49vhKL0pCfgK1maZofrZqKu1L0JGwDK/cjX5Z6NgY6495OFUuTUxIY8nobHRJRtVE3olLMh3YBQg0i7Iv1dKgw==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/debug@3.24.0': + resolution: {integrity: sha512-+3YZkr7NBRs7mgi6XayCIpjv+EmfmGwuvBQ96cMBzKSGRPyI1swhM59WVP1fobLECAnCNb1pX0Mx484rT5L2kw==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/model@3.24.0': + resolution: {integrity: sha512-lW4+35TQj6cyPNh5MxVxHHQaauQIhaCuHQYgpMinw7hPCaVhVhm1RhW5Zem6YJA/iDB7+4ffNSJZamb3jBZ8aA==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/private-build-infra@3.24.0': + resolution: {integrity: sha512-SEJ+hdjVK5y0NM6DpkSVKHqCxOvR2QXPQdJ0PXxbgFTxhyq4jGqT8N1Rl1zRKr9mg2WXFFwt4Lg9SBlYjqVHWg==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/record-data@3.24.0': + resolution: {integrity: sha512-hlk86tXo6v0h6JXg4B9OPl0lYWMHVNsoZ6i0qpmBHKuZKBI5cNmbmkPx9L2rs4lawy6Vf7NuYCm+QfuyG1IqCA==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/rfc395-data@0.0.4': + resolution: {integrity: sha512-tGRdvgC9/QMQSuSuJV45xoyhI0Pzjm7A9o/MVVA3HakXIImJbbzx/k/6dO9CUEQXIyS2y0fW6C1XaYOG7rY0FQ==} + + '@ember-data/serializer@3.24.0': + resolution: {integrity: sha512-nUCVJzIlTFQPVKqiODEZA6i7daPlnsPlWHyOOqcNbYnARTUCgRe8cnjehGtijYSrjXhvfy4iA7cPbw08dmtOhQ==} + engines: {node: 10.* || >= 12.*} + + '@ember-data/store@3.24.0': + resolution: {integrity: sha512-eyzHvqBDcQ/iVhYL82rP6YDiXy6H6w/ULGCwU8dr0ZAIDSKrWm5Nu4O3pSuhzNgxRTTE3JDCmMRDnGyeCJKKUQ==} + engines: {node: 10.* || >= 12.*} + + '@ember-decorators/component@6.1.1': + resolution: {integrity: sha512-Cj8tY/c0MC/rsipqsiWLh3YVN72DK92edPYamD/HzvftwzC6oDwawWk8RmStiBnG9PG/vntAt41l3S7HSSA+1Q==} + engines: {node: '>= 8.*'} + + '@ember-decorators/object@6.1.1': + resolution: {integrity: sha512-cb4CNR9sRoA31J3FCOFLDuR9ztM4wO9w1WlS4JeNRS7Z69SlB/XSXB/vplA3i9OOaXEy/zKWbu5ndZrHz0gvLw==} + engines: {node: '>= 8.*'} + + '@ember-decorators/utils@6.1.1': + resolution: {integrity: sha512-0KqnoeoLKb6AyoSU65TRF5T85wmS4uDn06oARddwNPxxf/lt5jQlh41uX3W7V/fWL9tPu8x1L1Vvpc80MN1+YA==} + engines: {node: '>= 8.*'} + + '@ember/edition-utils@1.2.0': + resolution: {integrity: sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==} + + '@ember/jquery@2.0.0': + resolution: {integrity: sha512-f8+WNqzXBNxl96jo0IwJBO5QCi0bnUlba9I7WbZcGhgnzszC76INJkw6l8UepZ1PMGG1H1wYpoIGoBBp5ZVmFA==} + engines: {node: 12.* || 14.* || >= 16} + + '@ember/optional-features@2.1.0': + resolution: {integrity: sha512-IXjDpTFhsjPk9h3OXwXjlRfhM/Wjtw2E71Xos/81ZsTTwZMB9H+DWhsxePXOkzYy7Jvw4TIzKbMfcnT8mrtwWQ==} + engines: {node: 10.* || 12.* || >= 14} + + '@ember/ordered-set@4.0.0': + resolution: {integrity: sha512-cUCcme4R5H37HyK8w0qzdG5+lpb3XVr2RQHLyWEP4JsKI66Ob4tizoJOs8rb/XdHCv+F5WeA321hfPMi3DrZbg==} + engines: {node: 10.* || 12.* || >= 14} + + '@ember/render-modifiers@2.1.0': + resolution: {integrity: sha512-LruhfoDv2itpk0fA0IC76Sxjcnq/7BC6txpQo40hOko8Dn6OxwQfxkPIbZGV0Cz7df+iX+VJrcYzNIvlc3w2EQ==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': ^1.0.2 + ember-source: ^3.8 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@glint/template': + optional: true + + '@ember/string@1.1.0': + resolution: {integrity: sha512-T8UHFSO9hrkRM9+OingBmbQ69mdb8xjEXxZLCNprQX+cEJI+dyI0Nv3JAYt/0SFTT+/IQW40r004O2n/CsNnEQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + '@ember/string@3.1.1': + resolution: {integrity: sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg==} + engines: {node: 12.* || 14.* || >= 16} + + '@ember/test-helpers@1.7.3': + resolution: {integrity: sha512-0NwxM9rtl/vD/Zqv8cHefLxojL3l2xuRs6pEppff/Fe39ybXz5H7hm5ZvnpRO/lpSsIeX7tivht9ko6/V+sShw==} + engines: {node: 6.* || 8.* || >= 10.*} + + '@ember/test-helpers@2.9.6': + resolution: {integrity: sha512-wUBB8e5nF24XSkl0TlRhHLs+WSf6yHimxDzo7L+a5n7mN5/omEdRkXMlm1qEp8N4+GNWfJKPHg9JTTm+9DA6uw==} + engines: {node: 10.* || 12.* || 14.* || 15.* || >= 16.*} + peerDependencies: + ember-source: '>=3.8.0' + + '@ember/test-waiters@3.1.0': + resolution: {integrity: sha512-bb9h95ktG2wKY9+ja1sdsFBdOms2lB19VWs8wmNpzgHv1NCetonBoV5jHBV4DHt0uS1tg9z66cZqhUVlYs96KQ==} + engines: {node: 10.* || 12.* || >= 14.*} + + '@embroider/addon-shim@1.10.2': + resolution: {integrity: sha512-EfI9cJ5/3QSUJtwm7x1MXrx3TEa2p7RNgSHefy7fvGm8/DP1xUFL25nST1NaHbHcqR1UhMlrTtv5iUIDoVzeQQ==} + engines: {node: 12.* || 14.* || >= 16} + + '@embroider/core@0.29.0': + resolution: {integrity: sha512-2i0QtV1y1jJpj1aiIA3FVZHfuLBN2yCUcJs0PkOsqZYi7J796KT4t7WwAk8gmBq00yGzHDWLw/iH4ULTomPS8A==} + engines: {node: 10.* || 12.* || >= 14} + + '@embroider/macros@0.29.0': + resolution: {integrity: sha512-Kg8we7U7TpgUZ0EBKlTC4UGItPa91OrGT5Bzxa2cJi/pPp1z8Amgd7Y+m29N+aLBZwlv+OxlhnOCm0Fhjw/dag==} + engines: {node: 10.* || 12.* || >= 14} + + '@embroider/macros@0.41.0': + resolution: {integrity: sha512-QISzwEEfLsskZeL0jyZDs1RoQSotwBWj+4upTogNHuxQP5j/9H3IMG/3QB1gh8GEpbudATb/cS4NDYK3UBxufw==} + engines: {node: 10.* || 12.* || >= 14} + + '@embroider/macros@1.16.13': + resolution: {integrity: sha512-2oGZh0m1byBYQFWEa8b2cvHJB2LzaF3DdMCLCqcRAccABMROt1G3sultnNCT30NhfdGWMEsJOT3Jm4nFxXmTRw==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': ^1.0.0 + peerDependenciesMeta: + '@glint/template': + optional: true + + '@embroider/macros@1.20.2': + resolution: {integrity: sha512-WJWSkG9vIL0s93vKwtNFqqAOCOflNkWNpqsC7VAqXeeTKNpCc7wtdOhPkNGJpb52CEt7vlQ5R/zMyCfGAB7MEA==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/template': ^1.0.0 + peerDependenciesMeta: + '@glint/template': + optional: true + + '@embroider/shared-internals@0.41.0': + resolution: {integrity: sha512-fiqUVB6cfh2UBEFE4yhT5EzagkZ1Q26+OhBV0nJszFEJZx4DqVIb3pxSSZ8P+HhpxuJsQ2XpMA/j02ZPFZfbdQ==} + engines: {node: 10.* || 12.* || >= 14} + + '@embroider/shared-internals@1.8.3': + resolution: {integrity: sha512-N5Gho6Qk8z5u+mxLCcMYAoQMbN4MmH+z2jXwQHVs859bxuZTxwF6kKtsybDAASCtd2YGxEmzcc1Ja/wM28824w==} + engines: {node: 12.* || 14.* || >= 16} + + '@embroider/shared-internals@2.9.0': + resolution: {integrity: sha512-8untWEvGy6av/oYibqZWMz/yB+LHsKxEOoUZiLvcpFwWj2Sipc0DcXeTJQZQZ++otNkLCWyDrDhOLrOkgjOPSg==} + engines: {node: 12.* || 14.* || >= 16} + + '@embroider/shared-internals@2.9.2': + resolution: {integrity: sha512-d96ub/WkS1Gx6dRDxZ0mCRPwFAHIMlMr2iti6uTYxTFzC85Wgt6j7bYr6ppkEuwEwKQVyzKRT0kTsJz6P74caQ==} + engines: {node: 12.* || 14.* || >= 16} + + '@embroider/shared-internals@3.0.2': + resolution: {integrity: sha512-/SusdG+zgosc3t+9sPFVKSFOYyiSgLfXOT6lYNWoG1YtnhWDxlK4S8leZ0jhcVjemdaHln5rTyxCnq8oFLxqpQ==} + engines: {node: 12.* || 14.* || >= 16} + + '@embroider/util@1.13.5': + resolution: {integrity: sha512-rHhGUzAQ5iOr5Swvk7yaarVe5SJtcjK2t/C8ts9agWfhTq4DVfy8+axF0KOf1jALRiJao3l9ALRGd6letKw2ZQ==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + '@glint/environment-ember-loose': ^1.0.0 + '@glint/template': ^1.0.0 + ember-source: '*' + peerDependenciesMeta: + '@glint/environment-ember-loose': + optional: true + '@glint/template': + optional: true + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.20.2': + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.20.2': + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.20.2': + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.20.2': + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.20.2': + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.20.2': + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.20.2': + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.20.2': + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.20.2': + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.20.2': + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.20.2': + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.20.2': + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.20.2': + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.20.2': + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.20.2': + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.20.2': + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.20.2': + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.20.2': + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.20.2': + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.20.2': + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.20.2': + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.20.2': + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.20.2': + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.16.0': + resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@9.37.0': + resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@extractus/oembed-extractor@3.2.1': + resolution: {integrity: sha512-ZwOY/SFqDph/1J4KfTTOKgGcy5xIrOSkK8b5KDHyoIg8AZ6+uEQ5l7SSNQskc4RxlLZGk+CBNOuRUGfkdPhKLA==} + engines: {node: '>= 15'} + + '@faker-js/faker@7.6.0': + resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + + '@faker-js/faker@8.4.1': + resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} + + '@faker-js/faker@9.9.0': + resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@fastify/otel@0.18.0': + resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@glimmer/component@1.1.2': + resolution: {integrity: sha512-XyAsEEa4kWOPy+gIdMjJ8XlzA3qrGH55ZDv6nA16ibalCR17k74BI0CztxuRds+Rm6CtbUVgheCVlcCULuqD7A==} + engines: {node: 6.* || 8.* || >= 10.*} + + '@glimmer/di@0.1.11': + resolution: {integrity: sha512-moRwafNDwHTnTHzyyZC9D+mUSvYrs1Ak0tRPjjmCghdoHHIvMshVbEnwKb/1WmW5CUlKc2eL9rlAV32n3GiItg==} + + '@glimmer/encoder@0.42.2': + resolution: {integrity: sha512-8xkdly0i0BP5HMI0suPB9ly0AnEq8x9Z8j3Gee1HYIovM5VLNtmh7a8HsaHYRs/xHmBEZcqtr8JV89w6F59YMQ==} + + '@glimmer/env@0.1.7': + resolution: {integrity: sha512-JKF/a9I9jw6fGoz8kA7LEQslrwJ5jms5CXhu/aqkBWk+PmZ6pTl8mlb/eJ/5ujBGTiQzBhy5AIWF712iA+4/mw==} + + '@glimmer/global-context@0.84.3': + resolution: {integrity: sha512-8Oy9Wg5IZxMEeAnVmzD2NkObf89BeHoFSzJgJROE/deutd3rxg83mvlOez4zBBGYwnTb+VGU2LYRpet92egJjA==} + + '@glimmer/interfaces@0.42.2': + resolution: {integrity: sha512-7LOuQd02cxxNNHChzdHMAU8/qOeQvTro141CU5tXITP7z6aOv2D2gkFdau97lLQiVxezGrh8J7h8GCuF7TEqtg==} + + '@glimmer/interfaces@0.84.3': + resolution: {integrity: sha512-dk32ykoNojt0mvEaIW6Vli5MGTbQo58uy3Epj7ahCgTHmWOKuw/0G83f2UmFprRwFx689YTXG38I/vbpltEjzg==} + + '@glimmer/interfaces@0.94.6': + resolution: {integrity: sha512-sp/1WePvB/8O+jrcUHwjboNPTKrdGicuHKA9T/lh0vkYK2qM5Xz4i25lQMQ38tEMiw7KixrjHiTUiaXRld+IwA==} + + '@glimmer/low-level@0.42.2': + resolution: {integrity: sha512-s+Q44SnKdTBTnkgX0deBlVNnNPVas+Pg8xEnwky9VrUqOHKsIZRrPgfVULeC6bIdFXtXOKm5CjTajhb9qnQbXQ==} + + '@glimmer/program@0.42.2': + resolution: {integrity: sha512-XpQ6EYzA1VL9ESKoih5XW5JftFmlRvwy3bF/I1ABOa3yLIh8mApEwrRI/sIHK0Nv5s1j0uW4itVF196WxnJXgw==} + + '@glimmer/reference@0.42.2': + resolution: {integrity: sha512-XuhbRjr3M9Q/DP892jGxVfPE6jaGGHu5w9ppGMnuTY7Vm/x+A+68MCiaREhDcEwJlzGg4UkfVjU3fdgmUIrc5Q==} + + '@glimmer/reference@0.84.3': + resolution: {integrity: sha512-lV+p/aWPVC8vUjmlvYVU7WQJsLh319SdXuAWoX/SE3pq340BJlAJiEcAc6q52y9JNhT57gMwtjMX96W5Xcx/qw==} + + '@glimmer/runtime@0.42.2': + resolution: {integrity: sha512-52LVZJsLKM3GzI3TEmYcw2LdI9Uk0jotISc3w2ozQBWvkKoYxjDNvI/gsjyMpenj4s7FcG2ggOq0x4tNFqm1GA==} + + '@glimmer/syntax@0.42.2': + resolution: {integrity: sha512-SR26SmF/Mb5o2cc4eLHpOyoX5kwwXP4KRhq4fbWfrvan74xVWA38PLspPCzwGhyVH/JsE7tUEPMjSo2DcJge/Q==} + + '@glimmer/syntax@0.84.3': + resolution: {integrity: sha512-ioVbTic6ZisLxqTgRBL2PCjYZTFIwobifCustrozRU2xGDiYvVIL0vt25h2c1ioDsX59UgVlDkIK4YTAQQSd2A==} + + '@glimmer/syntax@0.95.0': + resolution: {integrity: sha512-W/PHdODnpONsXjbbdY9nedgIHpglMfOzncf/moLVrKIcCfeQhw2vG07Rs/YW8KeJCgJRCLkQsi+Ix7XvrurGAg==} + + '@glimmer/tracking@1.1.2': + resolution: {integrity: sha512-cyV32zsHh+CnftuRX84ALZpd2rpbDrhLhJnTXn9W//QpqdRZ5rdMsxSY9fOsj0CKEc706tmEU299oNnDc0d7tA==} + + '@glimmer/util@0.42.2': + resolution: {integrity: sha512-Heck0baFSaWDanCYtmOcLeaz7v+rSqI8ovS7twrp2/FWEteb3Ze5sWQ2BEuSAG23L/k/lzVwYM/MY7ZugxBpaA==} + + '@glimmer/util@0.44.0': + resolution: {integrity: sha512-duAsm30uVK9jSysElCbLyU6QQYO2X9iLDLBIBUcCqck9qN1o3tK2qWiHbGK5d6g8E2AJ4H88UrfElkyaJlGrwg==} + + '@glimmer/util@0.84.3': + resolution: {integrity: sha512-qFkh6s16ZSRuu2rfz3T4Wp0fylFj3HBsONGXQcrAdZjdUaIS6v3pNj6mecJ71qRgcym9Hbaq/7/fefIwECUiKw==} + + '@glimmer/util@0.94.8': + resolution: {integrity: sha512-HfCKeZ74clF9BsPDBOqK/yRNa/ke6niXFPM6zRn9OVYw+ZAidLs7V8He/xljUHlLRL322kaZZY8XxRW7ALEwyg==} + + '@glimmer/validator@0.44.0': + resolution: {integrity: sha512-i01plR0EgFVz69GDrEuFgq1NheIjZcyTy3c7q+w7d096ddPVeVcRzU3LKaqCfovvLJ+6lJx40j45ecycASUUyw==} + + '@glimmer/validator@0.84.3': + resolution: {integrity: sha512-RTBV4TokUB0vI31UC7ikpV7lOYpWUlyqaKV//pRC4pexYMlmqnVhkFrdiimB/R1XyNdUOQUmnIAcdic39NkbhQ==} + + '@glimmer/vm@0.42.2': + resolution: {integrity: sha512-D2MNU5glICLqvet5SfVPrv+l6JNK2TR+CdQhch1Ew+btOoqlW+2LIJIF/5wLb1POjIMEkt+78t/7RN0mDFXGzw==} + + '@glimmer/wire-format@0.42.2': + resolution: {integrity: sha512-IqUo6mdJ7GRsK7KCyZxrc17ioSg9RBniEnb418ZMQxsV/WBv9NQ359MuClUck2M24z1AOXo4TerUw0U7+pb1/A==} + + '@glimmer/wire-format@0.94.8': + resolution: {integrity: sha512-A+Cp5m6vZMAEu0Kg/YwU2dJZXyYxVJs2zI57d3CP6NctmX7FsT8WjViiRUmt5abVmMmRH5b8BUovqY6GSMAdrw==} + + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + + '@gulpjs/to-absolute-glob@4.0.0': + resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==} + engines: {node: '>=10.13.0'} + + '@handlebars/parser@2.0.0': + resolution: {integrity: sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA==} + + '@handlebars/parser@2.2.2': + resolution: {integrity: sha512-n/SZW+12rwikx/f8YcSv9JCi5p9vn1Bnts9ZtVvfErG4h0gbjHI1H1ZMhVUnaOC7yzFc6PtsCKIK8XeTnL90Gw==} + engines: {node: ^18 || ^20 || ^22 || >=24} + + '@headlessui/react@1.7.19': + resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + + '@html-next/vertical-collection@3.0.0': + resolution: {integrity: sha512-kpLYZXr3tlYkrSiyhww+f1YkyLMEhCm9h55A+PWPzJXBTZkV12sC5mIbxVcWD7q5QNjy634m6MMvmxfFgDmGoQ==} + engines: {node: '>= 14.*'} + + '@html-validate/stylish@4.3.0': + resolution: {integrity: sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==} + engines: {node: '>= 18'} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + 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] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@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] + libc: [glibc] + + '@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] + libc: [glibc] + + '@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] + libc: [glibc] + + '@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] + libc: [glibc] + + '@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] + libc: [glibc] + + '@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] + libc: [glibc] + + '@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] + libc: [musl] + + '@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] + libc: [musl] + + '@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] + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@28.1.3': + resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@28.1.3': + resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@28.1.3': + resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@28.1.3': + resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@28.1.3': + resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0': + resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4': + resolution: {integrity: sha512-6PyZBYKnnVNqOSB0YFly+62R7dmov8segT27A+RVTBVd4iAE6kbW9QBJGlyR2yG4D4ohzhZSTIu7BK1UTtmFFA==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@kapouer/eslint-plugin-no-return-in-loop@1.0.0': + resolution: {integrity: sha512-IXQp8N68L2fkk7p7RckBBhT/KwAX04GooIGjwzmY5THQanQvsmJpYgwC7A1Io2XDXBJzlGelQkP/C1SRM/aq8w==} + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@keyvhq/core@2.1.15': + resolution: {integrity: sha512-YPc/vUECeKkWj1XSVT7ts7BQzU62RITjfru/Ft9uc+n2W0rm5mxTWzG51IK81g27+JrW+jI3OCUgNaeMCjTAMg==} + engines: {node: '>= 18'} + + '@keyvhq/memoize@2.0.3': + resolution: {integrity: sha512-ID5Br2OshdhyD4G0g1dm7915Ol2ee6RmCPMDSEh7QFtkbHrtzkgJfYS60na6tGWRBqViAMrCByBfD6uSinjAgg==} + engines: {node: '>= 16'} + + '@kikobeats/time-span@1.0.12': + resolution: {integrity: sha512-YeeiMfGaBKGoob/APJlRTZE1j8OL4cxKvjiOba49ED1IXlB+vj85q/aXMw/EZjEUMp5nBnfPmFoZEiwWLDNrYg==} + engines: {node: '>= 18'} + + '@lexical/clipboard@0.13.1': + resolution: {integrity: sha512-gMSbVeqb7S+XAi/EMMlwl+FCurLPugN2jAXcp5k5ZaUd7be8B+iupbYdoKkjt4qBhxmvmfe9k46GoC0QOPl/nw==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/code@0.13.1': + resolution: {integrity: sha512-QK77r3QgEtJy96ahYXNgpve8EY64BQgBSnPDOuqVrLdl92nPzjqzlsko2OZldlrt7gjXcfl9nqfhZ/CAhStfOg==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/headless@0.13.1': + resolution: {integrity: sha512-W2mLUuWPrsyf2n73NWM8nKiBI11lEpVVzKE0OzMsjTskv5+AAMaeu1wQ7M1508vKdCcUZwA6AOh3To/hstLEpw==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/html@0.13.1': + resolution: {integrity: sha512-XkZrnCSHIUavtpMol6aG8YsJ5KqC9hMxEhAENf3HTGi3ocysCByyXOyt1EhEYpjJvgDG4wRqt25xGDbLjj1/sA==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/link@0.13.1': + resolution: {integrity: sha512-7E3B2juL2UoMj2n+CiyFZ7tlpsdViAoIE7MpegXwfe/VQ66wFwk/VxGTa/69ng2EoF7E0kh+SldvGQDrWAWb1g==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/list@0.13.1': + resolution: {integrity: sha512-6U1pmNZcKLuOWiWRML8Raf9zSEuUCMlsOye82niyF6I0rpPgYo5UFghAAbGISDsyqzM1B2L4BgJ6XrCk/dJptg==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/rich-text@0.13.1': + resolution: {integrity: sha512-HliB9Ync06mv9DBg/5j0lIsTJp+exLHlaLJe+n8Zq1QNTzZzu2LsIT/Crquk50In7K/cjtlaQ/d5RB0LkjMHYg==} + peerDependencies: + '@lexical/clipboard': 0.13.1 + '@lexical/selection': 0.13.1 + '@lexical/utils': 0.13.1 + lexical: 0.13.1 + + '@lexical/selection@0.13.1': + resolution: {integrity: sha512-Kt9eSwjxPznj7yzIYipu9yYEgmRJhHiq3DNxHRxInYcZJWWNNHum2xKyxwwcN8QYBBzgfPegfM/geqQEJSV1lQ==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/table@0.13.1': + resolution: {integrity: sha512-VQzgkfkEmnvn6C64O/kvl0HI3bFoBh3WA/U67ALw+DS11Mb5CKjbt0Gzm/258/reIxNMpshjjicpWMv9Miwauw==} + peerDependencies: + lexical: 0.13.1 + + '@lexical/utils@0.13.1': + resolution: {integrity: sha512-AtQQKzYymkbOaQxaBXjRBS8IPxF9zWQnqwHTUTrJqJ4hX71aIQd/thqZbfQETAFJfC8pNBZw5zpxN6yPHk23dQ==} + peerDependencies: + lexical: 0.13.1 + + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + + '@lezer/css@1.3.3': + resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lint-todo/utils@13.1.1': + resolution: {integrity: sha512-F5z53uvRIF4dYfFfJP3a2Cqg+4P1dgJchJsFnsZE0eZp0LK8X7g2J0CsJHRgns+skpXOlM7n5vFGwkWCWj8qJg==} + engines: {node: 12.* || >= 14} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@metascraper/helpers@5.50.0': + resolution: {integrity: sha512-w5eJO9sblf5btT0qSenQf8IlrHzifY7F7yTBB6q0RH+6gXgb9KgLno8WwWDjNeWkr+3n0D+8v0Ty5YGKGLAmyw==} + engines: {node: '>= 22'} + + '@miragejs/pretender-node-polyfill@0.1.2': + resolution: {integrity: sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g==} + + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/agent@4.0.0': + resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@number-flow/react@0.5.10': + resolution: {integrity: sha512-a8Wh5eNITn7Km4xbddAH7QH8eNmnduR6k34ER1hkHSGO4H2yU1DDnuAWLQM99vciGInFODemSc0tdxrXkJEpbA==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + + '@nx/nx-darwin-arm64@22.0.4': + resolution: {integrity: sha512-CELBI9syCax+YTgiExafA5vHdfCklh6E19PRcZMjKi3j+ZX54pF3L2v769+SLe4cX4DwY9rOsghJbLDM2qU4tw==} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@22.0.4': + resolution: {integrity: sha512-p+pmlq/mdNhQb12RwHP9V6yAUX9CLy8GUT4ijPzFTbxqa9dZbJk69NpSRwpAhAvvQ30gp1Zyh0t0/k/yaZqMIg==} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@22.0.4': + resolution: {integrity: sha512-XW2SXtfO245DRnAXVGYJUB7aBJsJ2rPD5pizxJET+l3VmtHGp2crdVuftw6iqjgrf2eAS+yCe61Jnqh687vWFg==} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@22.0.4': + resolution: {integrity: sha512-LCLuhbW3SIFz2FGiLdspCrNP889morCzTV/pEtxA8EgusWqCR8WjeSj3QvN8HN/GoXDsJxoUXvClZbHE+N6Hyg==} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@22.0.4': + resolution: {integrity: sha512-2jvS8MYYOI8eUBRTmE8HKm5mRVLqS5Cvlj06tEAjxrmH5d7Bv8BG5Ps9yZzT0qswfVKChpzIliwPZomUjLTxmA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-arm64-musl@22.0.4': + resolution: {integrity: sha512-IK9gf8/AOtTW6rZajmGAFCN7EBzjmkIevt9MtOehQGlNXlMXydvUYKE5VU7d4oglvYs8aJJyayihfiZbFnTS8g==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@nx/nx-linux-x64-gnu@22.0.4': + resolution: {integrity: sha512-CdALjMqqNgiffQQIlyxx6mrxJCOqDzmN6BW3w9msCPHVSPOPp4AenlT0kpC7ALvmNEUm0lC4r093QbN2t6a/wA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@nx/nx-linux-x64-musl@22.0.4': + resolution: {integrity: sha512-2GPy+mAQo4JnfjTtsgGrHhZbTmmGy4RqaGowe0qMYCMuBME33ChG9iiRmArYmVtCAhYZVn26rK76/Vn3tK7fgg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@nx/nx-win32-arm64-msvc@22.0.4': + resolution: {integrity: sha512-jnZCCnTXoqOIrH0L31+qHVHmJuDYPoN6sl37/S1epP9n4fhcy9tjSx4xvx/WQSd417lU9saC+g7Glx2uFdgcTw==} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@22.0.4': + resolution: {integrity: sha512-CDBqgb9RV5aHMDLcsS9kDDULc38u/eieZBhHBL01Ca5Tq075QuHn4uly6sYyHwVOxrhY4eaWNSfV2xG3Bg6Gtw==} + cpu: [x64] + os: [win32] + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@opentelemetry/api-logs@0.207.0': + resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.6.1': + resolution: {integrity: sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-amqplib@0.61.0': + resolution: {integrity: sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.57.0': + resolution: {integrity: sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.31.0': + resolution: {integrity: sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.62.0': + resolution: {integrity: sha512-Tvx+vgAZKEQxU3Rx+xWLiR0mLxHwmk69/8ya04+VsV9WYh8w6Lhx5hm5yAMvo1wy0KqWgFKBLwSeo3sHCwdOww==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.33.0': + resolution: {integrity: sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.57.0': + resolution: {integrity: sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.62.0': + resolution: {integrity: sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.60.0': + resolution: {integrity: sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.214.0': + resolution: {integrity: sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.62.0': + resolution: {integrity: sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.23.0': + resolution: {integrity: sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.58.0': + resolution: {integrity: sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.62.0': + resolution: {integrity: sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0': + resolution: {integrity: sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.67.0': + resolution: {integrity: sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.60.0': + resolution: {integrity: sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.60.0': + resolution: {integrity: sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.60.0': + resolution: {integrity: sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.66.0': + resolution: {integrity: sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.62.0': + resolution: {integrity: sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.33.0': + resolution: {integrity: sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.24.0': + resolution: {integrity: sha512-oKzZ3uvqP17sV0EsoQcJgjEfIp0kiZRbYu/eD8p13Cbahumf8lb/xpYeNr/hfAJ4owzEtIDcGIjprfLcYbIKBQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.207.0': + resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.38.2': + resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resources@2.6.1': + resolution: {integrity: sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.6.1': + resolution: {integrity: sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} + engines: {node: '>=18'} + hasBin: true + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@prettier/sync@0.6.1': + resolution: {integrity: sha512-yF9G8vK/LYUTF3Cijd7VC9La3b20F20/J/fgoR4H0B8JGOWnZVZX6+I6+vODPosjmMcpdlUV+gUqJQZp3kLOcw==} + peerDependencies: + prettier: '*' + + '@prisma/instrumentation@7.6.0': + resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==} + peerDependencies: + '@opentelemetry/api': ^1.8 + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@radix-ui/number@1.0.1': + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.0.3': + resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.11': + resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.0.3': + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.0.1': + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.0.4': + resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.0.3': + resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.1.2': + resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.0.3': + resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@1.2.2': + resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.0.1': + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.0.1': + resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.0.1': + resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.0.3': + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.0.1': + resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rolldown/pluginutils@1.0.0-beta.35': + resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@selderee/plugin-htmlparser2@0.6.0': + resolution: {integrity: sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA==} + + '@sentry-internal/feedback@7.120.3': + resolution: {integrity: sha512-ewJJIQ0mbsOX6jfiVFvqMjokxNtgP3dNwUv+4nenN+iJJPQsM6a0ocro3iscxwVdbkjw5hY3BUV2ICI5Q0UWoA==} + engines: {node: '>=12'} + + '@sentry-internal/feedback@7.120.4': + resolution: {integrity: sha512-eSwgvTdrh03zYYaI6UVOjI9p4VmKg6+c2+CBQfRZX++6wwnCVsNv7XF7WUIpVGBAkJ0N2oapjQmCzJKGKBRWQg==} + engines: {node: '>=12'} + + '@sentry-internal/replay-canvas@7.120.3': + resolution: {integrity: sha512-s5xy+bVL1eDZchM6gmaOiXvTqpAsUfO7122DxVdEDMtwVq3e22bS2aiGa8CUgOiJkulZ+09q73nufM77kOmT/A==} + engines: {node: '>=12'} + + '@sentry-internal/replay-canvas@7.120.4': + resolution: {integrity: sha512-2+W4CgUL1VzrPjArbTid4WhKh7HH21vREVilZdvffQPVwOEpgNTPAb69loQuTlhJVveh9hWTj2nE5UXLbLP+AA==} + engines: {node: '>=12'} + + '@sentry-internal/tracing@7.116.0': + resolution: {integrity: sha512-y5ppEmoOlfr77c/HqsEXR72092qmGYS4QE5gSz5UZFn9CiinEwGfEorcg2xIrrCuU7Ry/ZU2VLz9q3xd04drRA==} + engines: {node: '>=8'} + + '@sentry-internal/tracing@7.120.3': + resolution: {integrity: sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==} + engines: {node: '>=8'} + + '@sentry-internal/tracing@7.120.4': + resolution: {integrity: sha512-Fz5+4XCg3akeoFK+K7g+d7HqGMjmnLoY2eJlpONJmaeT9pXY7yfUyXKZMmMajdE2LxxKJgQ2YKvSCaGVamTjHw==} + engines: {node: '>=8'} + + '@sentry/browser@7.120.3': + resolution: {integrity: sha512-i9vGcK9N8zZ/JQo1TCEfHHYZ2miidOvgOABRUc9zQKhYdcYQB2/LU1kqlj77Pxdxf4wOa9137d6rPrSn9iiBxg==} + engines: {node: '>=8'} + + '@sentry/browser@7.120.4': + resolution: {integrity: sha512-ymlNtIPG6HAKzM/JXpWVGCzCNufZNADfy+O/olZuVJW5Be1DtOFyRnBvz0LeKbmxJbXb2lX/XMhuen6PXPdoQw==} + engines: {node: '>=8'} + + '@sentry/core@10.47.0': + resolution: {integrity: sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==} + engines: {node: '>=18'} + + '@sentry/core@7.114.0': + resolution: {integrity: sha512-YnanVlmulkjgZiVZ9BfY9k6I082n+C+LbZo52MTvx3FY6RE5iyiPMpaOh67oXEZRWcYQEGm+bKruRxLVP6RlbA==} + engines: {node: '>=8'} + + '@sentry/core@7.116.0': + resolution: {integrity: sha512-J6Wmjjx+o7RwST0weTU1KaKUAlzbc8MGkJV1rcHM9xjNTWTva+nrcCM3vFBagnk2Gm/zhwv3h0PvWEqVyp3U1Q==} + engines: {node: '>=8'} + + '@sentry/core@7.120.3': + resolution: {integrity: sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==} + engines: {node: '>=8'} + + '@sentry/core@7.120.4': + resolution: {integrity: sha512-TXu3Q5kKiq8db9OXGkWyXUbIxMMuttB5vJ031yolOl5T/B69JRyAoKuojLBjRv1XX583gS1rSSoX8YXX7ATFGA==} + engines: {node: '>=8'} + + '@sentry/ember@7.120.3': + resolution: {integrity: sha512-R7mS2STQe/9qNypggiRJEAP6QyZHm/cK2oew2IT2lLxuLsiFHNHG5qeQ2inlAYQ03QBzpQ1/ffC2qSHX7jArAA==} + engines: {node: 14.* || 16.* || >= 18} + + '@sentry/integrations@7.114.0': + resolution: {integrity: sha512-BJIBWXGKeIH0ifd7goxOS29fBA8BkEgVVCahs6xIOXBjX1IRS6PmX0zYx/GP23nQTfhJiubv2XPzoYOlZZmDxg==} + engines: {node: '>=8'} + + '@sentry/integrations@7.120.3': + resolution: {integrity: sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==} + engines: {node: '>=8'} + + '@sentry/integrations@7.120.4': + resolution: {integrity: sha512-kkBTLk053XlhDCg7OkBQTIMF4puqFibeRO3E3YiVc4PGLnocXMaVpOSCkMqAc1k1kZ09UgGi8DxfQhnFEjUkpA==} + engines: {node: '>=8'} + + '@sentry/node-core@10.47.0': + resolution: {integrity: sha512-qv6LsqHbkQmd0aQEUox/svRSz26J+l4gGjFOUNEay2armZu9XLD+Ct89jpFgZD5oIPNAj2jraodTRqydXiwS5w==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/resources': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/context-async-hooks': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/resources': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/semantic-conventions': + optional: true + + '@sentry/node@10.47.0': + resolution: {integrity: sha512-R+btqPepv88o635G6HtVewLjqCLUedBg5HBs7Nq1qbbKvyti01uArUF2f+3DsLenk5B9LUNiRlE+frZA44Ahmw==} + engines: {node: '>=18'} + + '@sentry/node@7.120.4': + resolution: {integrity: sha512-qq3wZAXXj2SRWhqErnGCSJKUhPSlZ+RGnCZjhfjHpP49KNpcd9YdPTIUsFMgeyjdh6Ew6aVCv23g1hTP0CHpYw==} + engines: {node: '>=8'} + + '@sentry/opentelemetry@10.47.0': + resolution: {integrity: sha512-f6Hw2lrpCjlOksiosP0Z2jK/+l+21SIdoNglVeG/sttMyx8C8ywONKh0Ha50sFsvB1VaB8n94RKzzf3hkh9V3g==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + + '@sentry/react@7.120.4': + resolution: {integrity: sha512-Pj1MSezEncE+5riuwsk8peMncuz5HR72Yr1/RdZhMZvUxoxAR/tkwD3aPcK6ddQJTagd2TGwhdr9SHuDLtONew==} + engines: {node: '>=8'} + peerDependencies: + react: 15.x || 16.x || 17.x || 18.x + + '@sentry/replay@7.116.0': + resolution: {integrity: sha512-OrpDtV54pmwZuKp3g7PDiJg6ruRMJKOCzK08TF7IPsKrr4x4UQn56rzMOiABVuTjuS8lNfAWDar6c6vxXFz5KA==} + engines: {node: '>=12'} + + '@sentry/replay@7.120.3': + resolution: {integrity: sha512-CjVq1fP6bpDiX8VQxudD5MPWwatfXk8EJ2jQhJTcWu/4bCSOQmHxnnmBM+GVn5acKUBCodWHBN+IUZgnJheZSg==} + engines: {node: '>=12'} + + '@sentry/replay@7.120.4': + resolution: {integrity: sha512-FW8sPenNFfnO/K7sncsSTX4rIVak9j7VUiLIagJrcqZIC7d1dInFNjy8CdVJUlyz3Y3TOgIl3L3+ZpjfyMnaZg==} + engines: {node: '>=12'} + + '@sentry/types@7.114.0': + resolution: {integrity: sha512-tsqkkyL3eJtptmPtT0m9W/bPLkU7ILY7nvwpi1hahA5jrM7ppoU0IMaQWAgTD+U3rzFH40IdXNBFb8Gnqcva4w==} + engines: {node: '>=8'} + + '@sentry/types@7.116.0': + resolution: {integrity: sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ==} + engines: {node: '>=8'} + + '@sentry/types@7.120.3': + resolution: {integrity: sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==} + engines: {node: '>=8'} + + '@sentry/types@7.120.4': + resolution: {integrity: sha512-cUq2hSSe6/qrU6oZsEP4InMI5VVdD86aypE+ENrQ6eZEVLTCYm1w6XhW1NvIu3UuWh7gZec4a9J7AFpYxki88Q==} + engines: {node: '>=8'} + + '@sentry/utils@7.114.0': + resolution: {integrity: sha512-319N90McVpupQ6vws4+tfCy/03AdtsU0MurIE4+W5cubHME08HtiEWlfacvAxX+yuKFhvdsO4K4BB/dj54ideg==} + engines: {node: '>=8'} + + '@sentry/utils@7.116.0': + resolution: {integrity: sha512-Vn9fcvwTq91wJvCd7WTMWozimqMi+dEZ3ie3EICELC2diONcN16ADFdzn65CQQbYwmUzRjN9EjDN2k41pKZWhQ==} + engines: {node: '>=8'} + + '@sentry/utils@7.120.3': + resolution: {integrity: sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==} + engines: {node: '>=8'} + + '@sentry/utils@7.120.4': + resolution: {integrity: sha512-zCKpyDIWKHwtervNK2ZlaK8mMV7gVUijAgFeJStH+CU/imcdquizV3pFLlSQYRswG+Lbyd6CT/LGRh3IbtkCFw==} + engines: {node: '>=8'} + + '@sidvind/better-ajv-errors@3.0.1': + resolution: {integrity: sha512-++1mEYIeozfnwWI9P1ECvOPoacy+CgDASrmGvXPMCcqgx0YUzB01vZ78uHdQ443V6sTY+e9MzHqmN9DOls02aw==} + engines: {node: '>= 16.14'} + peerDependencies: + ajv: ^6.12.3 || ^7.0.0 || ^8.0.0 + + '@simple-dom/document@1.4.0': + resolution: {integrity: sha512-/RUeVH4kuD3rzo5/91+h4Z1meLSLP66eXqpVAw/4aZmYozkeqUkMprq0znL4psX/adEed5cBgiNJcfMz/eKZLg==} + + '@simple-dom/interface@1.4.0': + resolution: {integrity: sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==} + + '@simple-dom/parser@1.4.0': + resolution: {integrity: sha512-TNjDkOehueRIKr1df416qk9ELj+qWuVVJNIT25y1aZg3pQvxv4UPGrgaDFte7dsWBTbF3V8NYPNQ5FDUZQ8Wlg==} + + '@simple-dom/serializer@1.4.0': + resolution: {integrity: sha512-mI1yRahsVs8atXLiQksineDsFEFqeG7RHwnnBTDOK6inbzl4tZQgjR+Z7edjgIJq5j5RhZvwPI6EuCji9B3eQw==} + + '@simple-dom/void-map@1.4.0': + resolution: {integrity: sha512-VDhLEyVCbuhOBBgHol9ShzIv9O8UCzdXeH4FoXu2DOcu/nnvTjLTck+BgXsCLv5ynDiUdoqsREEVFnoyPpFKVw==} + + '@sinclair/typebox@0.24.51': + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sinonjs/commons@1.8.6': + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@sinonjs/fake-timers@11.2.2': + resolution: {integrity: sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==} + + '@sinonjs/fake-timers@15.1.1': + resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} + + '@sinonjs/fake-timers@15.3.1': + resolution: {integrity: sha512-oDDGPn/4jD3viZLphixgu1jwT0bqIqP25FNXC5OkWrUqHZOF4wATtSyVzluOt4DqcMqSoKMClyhUllKSxpQCng==} + + '@sinonjs/fake-timers@6.0.1': + resolution: {integrity: sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==} + + '@sinonjs/samsam@10.0.1': + resolution: {integrity: sha512-q2mHXfkviqX+roGbzFJF6r5GR4TJDGngJuPrtj0IZRZZGnFlYTjdeoiZ6vCISmOjTLsZm0+CqHPupXR/BRVZjA==} + + '@sinonjs/samsam@5.3.1': + resolution: {integrity: sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==} + + '@sinonjs/samsam@8.0.3': + resolution: {integrity: sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==} + + '@sinonjs/text-encoding@0.7.3': + resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} + deprecated: |- + Deprecated: no longer maintained and no longer used by Sinon packages. See + https://github.com/sinonjs/nise/issues/243 for replacement details. + + '@slack/types@2.20.1': + resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/webhook@7.0.9': + resolution: {integrity: sha512-hMfkQ5Y3Y7FtL+ZYhcxFblidx4Z2LPRFrhY1KJb6NqQdnK6kzTzTS1mjH5taVQIB496eqwpg9FE9mq9BFx0DWw==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@smithy/abort-controller@4.2.12': + resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.13': + resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.12': + resolution: {integrity: sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.13': + resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.12': + resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.12': + resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.12': + resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.12': + resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.12': + resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.12': + resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.15': + resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.13': + resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.12': + resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.12': + resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.12': + resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.12': + resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.12': + resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.27': + resolution: {integrity: sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.28': + resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.44': + resolution: {integrity: sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.4.46': + resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.15': + resolution: {integrity: sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.16': + resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.12': + resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.12': + resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.0': + resolution: {integrity: sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.1': + resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.12': + resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.12': + resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.12': + resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.12': + resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.12': + resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.7': + resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.12': + resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.7': + resolution: {integrity: sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.8': + resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.13.1': + resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.12': + resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.43': + resolution: {integrity: sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.44': + resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.47': + resolution: {integrity: sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.48': + resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.3': + resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.12': + resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.12': + resolution: {integrity: sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.2.13': + resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.20': + resolution: {integrity: sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.21': + resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.13': + resolution: {integrity: sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.14': + resolution: {integrity: sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@stdlib/array-float32@0.2.3': + resolution: {integrity: sha512-LxdKGrpsCehFDgU+nw7r3/NL+g8pe9zcDn/6fvNwiuy0AiunX/+c8lin9qUy/FEhrbU6aFXepFM2Ql/9YQD+TA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-float64@0.2.3': + resolution: {integrity: sha512-LtQOcxfE2zX5QnYfLNfptSV9q3JUGkOvGhvtO94iPXEvYTvJSEWLK7G97AJR9MK3rdL1+USjGQzUPiUd1/kAbw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-int16@0.2.3': + resolution: {integrity: sha512-yNoSOvDGUwHlO5g3wyEM5KYIRaHB5N/PNQHhOTbuvjdsvBhPgmyJr/TQABnSipKsS6hOEveI1SxyONfZGD2QkQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-int32@0.2.3': + resolution: {integrity: sha512-m+uRYfsP2XDMCgCN0EMsxUNha4SQ29htPqFC9KZPgnVvvmjr5gsTHiZ95Io/ErgLxFCjRajpi9stjZ7f9Xeiug==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-int8@0.2.3': + resolution: {integrity: sha512-RZFDdqpFwNrIUob38519AL6tNWGQb/Nqo5TnocCgLPRVFa722DF2MI/DMjhQBbp9RhI2gHAz/gtqfa3CIhsM7Q==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-uint16@0.2.3': + resolution: {integrity: sha512-ceDGvNGOxR2kUO30USUWL8ezKw+R6wBwQNJmPOfT1HJRmCxio+eRMBygmxoRmWwoj2pj8IHC/GlC3N2kSNN0Iw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-uint32@0.2.3': + resolution: {integrity: sha512-zZGjkJjPsgp3WyKOCICOGaJ3OoVyzwgyZgyEHl3wlEqfPhUPpVJGLhy9mFx8WIoTGHsCWHs1COWZfh88bL/pSA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-uint8@0.2.3': + resolution: {integrity: sha512-+UVsdR94Qe1zXEbpE+rpXFxp/VB8+zGxH3d7RH2cUpy5eFeqcSQNvxaxfqZnbmqk2Gu5tJEv/ZBcTXkpeKH7HA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/array-uint8c@0.2.3': + resolution: {integrity: sha512-bTK5NJeXPo3bpiSu7qebFjGogu9CJKCNmmooAjPcq1Kj/S5C+vVamJjHx/C32euvlynwRiF0fJjcerSVvggsZQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-float32array-support@0.2.3': + resolution: {integrity: sha512-nNRO8I4dp3CtxFXCD901IBmVc+/otaFMr/Mx1jN956nkaXaL3GZR6p12IGgN535Nj3QILoKPmOG7gOjts+uQOA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-float64array-support@0.2.3': + resolution: {integrity: sha512-BbHjShic68woFydK2s8ECn4uKM+Cr3lB6kBP+9wK7wkUGVzq4lo7n+Twt0cBbgwJOGLlQayJQOQAVVq/nu/wDg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-int16array-support@0.2.3': + resolution: {integrity: sha512-b/2LkEPpg4jmrZZYVF0kmjsThEolINpj7TkKLQORxWeFKlULHX+rEVBwnUxGqW7Z3P6xwklbSlzzhlCU0xXc4w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-int32array-support@0.2.3': + resolution: {integrity: sha512-FjGlpOYwUV/DbWpX14ZirgxMRgkwe6KKsN+/DEllu/w0bLIR4uBVBOQk8XDnExSVd72CiOH9+HOx6QTjHlhnMg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-int8array-support@0.2.3': + resolution: {integrity: sha512-DYq1D2vpVO9NnRQa+BQ6JeZy4SX4yps2Tusalnri7M9s9BNzgzH2rqB1gdcUmSLQ8vLLaHNVsHPGoiPqh9tJBw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-node-buffer-support@0.2.3': + resolution: {integrity: sha512-NwoVd9SYFeVbcuwBlG2l6REe/uTPx44aedIB1+LOfMA0MC3PFUxarF7ivuqksPGBLbIuD7olDm8uCW19lAvJhw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-own-property@0.2.3': + resolution: {integrity: sha512-BnX1Lpvd9YaucQLokQrf7ppLwa3V+nTUQ9qoys5SloYhjwbYWtO61SJVT2JK+cfsCxUAx/HAh3dN1qTKxx1PIg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-symbol-support@0.2.3': + resolution: {integrity: sha512-yNsJnCb7HWye5xhZ/eRIpp28HwOVFF5gUCKeT1p4haoDJOn+3YEPYaXMn4mYK6kRN96tUhovlJQv9H/9jwnLpA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-to-primitive-symbol-support@0.1.1': + resolution: {integrity: sha512-nobeU8aB76XlDtKCC4DZWRdWSbPCoOGOOwEuVoyJ058gNacR4lRPoCA/7cxAVl3UqqLwhX3E1MSOMxNTnMyTGA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-tostringtag-support@0.2.3': + resolution: {integrity: sha512-9sULfRKYneF/Mq6F96xqeQrf0a9wdJc1lNZf2wfs1EtRwQHQfx5+QlYl7hp3gWI/iid5M4Npme86T0413eaBfA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-uint16array-support@0.2.3': + resolution: {integrity: sha512-7PjQTyXmpIczUz7Vx8c7BcdroJmMmQjyNQDF9a46Ya+nZxqH7g9jRsxmvzqVrSyXhkIyAbw1Ao2QyKocz+hZhg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-uint32array-support@0.2.3': + resolution: {integrity: sha512-xa/0yWs+fhp/yUZPD3/ZmQCXqv7haJGGJE75HBX9tw9CHgmqpgAihD4yGy982CtQaxEltH8icNOX8CQzZZS31w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-uint8array-support@0.2.3': + resolution: {integrity: sha512-cgXcNtv5PS+LEV0tj8VOZ8WVjLJuvSzG0GolF+2M0u5le2hHk7BfKH9bqmrk0AzpipAEEdQgp4CGc/Mmji5s8g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-has-uint8clampedarray-support@0.2.3': + resolution: {integrity: sha512-Lp2sWfsU5Saqm4IQ+ARuUFc3o6qOJoYxzhyI9nAHC1yy8bgvwNkjFiGs+fJcfrtFxeNDC8WIs+IbzuiT3HSoJw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-arguments@0.2.3': + resolution: {integrity: sha512-J3cf5pEZmJkjUi2MV/uldPe+rOpC9Y7pLAtJHY7BkCTKxZCyn16KpX9PuqeMXp+H04GdD9wxuhESGdqoXPa7pg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-array@0.2.3': + resolution: {integrity: sha512-ayRsLGmssNO+8SR63tPP2+LZCxFVsSXtdN0ToSdm0kAOaGT4e0FoQus+AlFdhW5xWtvoxL8r5WKTVqJ514/rvQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-big-endian@0.2.3': + resolution: {integrity: sha512-+/X7ZcXFdboHIKDtxnx25fKmf9EhPRyp37qp9H9tUb0RnPT8SgmQKtYZxovp7EeBbESZOKSousg0jZKLbPqRAg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-boolean@0.2.3': + resolution: {integrity: sha512-36BETlRDrBeCOBLowW1/vqk9wD+OA3Wp9eQxFij/7icKiMDURH+Fiw2hfbhaZ3c3Y9uEN0rAdOT7AJlwWhXc7Q==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-buffer@0.2.3': + resolution: {integrity: sha512-ppnL0sC5XHSk221AYjT4zD33TDB/XuSkhbbbr5sviTH5+88KAqy7SjFlvcpqpvB2BvEVzqRE1fgVso8SjGQD+A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-collection@0.2.3': + resolution: {integrity: sha512-mAL86cRZhTeqCHiLDynOFm2S3DRUaB9NWPbUSFELmoWZ9wV0JeCIfNRYRyzWRPthMsyTYACAhLx8UV8IGJjsmQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-enumerable-property@0.2.3': + resolution: {integrity: sha512-UtqsBVUEAnPjfbOduQ8CT/YwUFLiXiapjSnKaGeLX000qBHCW07uOL1nIuE/vN61SMB+yvZuTZl9xUux/Adtew==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-error@0.2.3': + resolution: {integrity: sha512-ibTQFux4OmQL9GAYt/mp5SSohUw7GdXLU0prYw6/JVB0qgfJhOFjBf1q/ZYzDSHI8J+RglvVI4ZQuh8ocico8Q==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-float32array@0.2.3': + resolution: {integrity: sha512-fHPem8taZPd1YoYIB1lqju9HEPDUuY/UFKG0/PquCZtA/g32TEyMM7tb/GCcDQuxno9jnH4+6r7CTfHaa9758w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-float64array@0.2.3': + resolution: {integrity: sha512-GEuYmjWDT2PY530fIO4qBDr4xO9vYWTjyszkyma0RWKzyGBdCV6GYdMj8qO+W7bOVsqeuoYNTCNebOYfpgZbSQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-function@0.2.3': + resolution: {integrity: sha512-6EdSkdJ1KtILdGb84MhherNtitGgzHH+u6rs1gGj+294wg/9IzLYaaHDktJVRN/viD3XLEQjPGzSTq5x5DzPbQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-int16array@0.2.3': + resolution: {integrity: sha512-H5OohxNtQJ/i6esWE5R01TmXl0M5gCdvHP3+02ahiXbq6PBOJ5bJQVvw2M/j0jO93tS6d+5OcVGAKGazSn76RQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-int32array@0.2.3': + resolution: {integrity: sha512-ATPkqHYsQnOYs2Vz3KudJd/N79GFsiKzEOiTMzl5ILcN6bdW28ZNjfh2eIJ/o8NR22rfPQOiUCvxJHw2hpAYKw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-int8array@0.2.3': + resolution: {integrity: sha512-eK9SvSUVTTQ0yGgWF9BI/hvqs6pu77NoVRwfQLLvjzcpI3hRm5RyzhHyoNe6Q7FQ1obxvVYsCjysRmC+xmV36Q==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-integer@0.2.3': + resolution: {integrity: sha512-HfJg0x6YWW6aP3JSONoI5bNXFGqtiNqWj0L0ex+oDpzrVbXQOu9GNlRKP5P5cDgfI+3rpZMCINX52IGKyCiUJw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-little-endian@0.2.3': + resolution: {integrity: sha512-TvKdQhKwrWf0rik5iiurlI1hpJru+bbYGByoSJ6SScTvZIgf54MTYEsWgVyoNqMw8AQFxOyDbMRee+je65ZbMA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-nan@0.2.3': + resolution: {integrity: sha512-gFTy7KCz0/bqf0pnEBihtpVz8QYZHiYDuv8wvTZsybMz8tDOE5LnNPJ1klCsWgRLgCy9MA7uec3xwfst9n+71A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-nonnegative-integer@0.2.3': + resolution: {integrity: sha512-3Goiu4Kum1Xm9NbvC23GZOj1a6aVrGF1D8CkZkKDNp8kiYgKSKonvcbf3JeVrr/fLxsdfjnprbzrPOYsMOsRYw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-number@0.2.3': + resolution: {integrity: sha512-6i2RoG5TYn7mfKnPmvAA1Gdhn3PxvQegb2bh2pi5k/+xXgEHoubpqBSVxZBTZ70GIkPwNFJQDRWqwOwHet6enQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-object-like@0.2.3': + resolution: {integrity: sha512-K6G58h5euEVSEZvFZSHADoYL7sixNTRy+ezHl/I3byJpC9PJdvTO0XWYD0CT0yVXIaqmCYU2NsPpgGdcPLELFg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-object@0.2.3': + resolution: {integrity: sha512-H9NaVGuPl4qA3J4gDAzy+sHCkTImVycoNd7izPF63JuAlEO6KTdpVK7stQBXgcWf1pyRHem7ZTJCzht2UMLzcA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-plain-object@0.2.3': + resolution: {integrity: sha512-MRdn9kuzC4Hf/2Z/911yZUso6s6f048Ck3dO1j9scHk9EKnU404XrC5NJHeZ9OaI/689Fs+JKDWyKyqyHYRV8A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-regexp@0.2.3': + resolution: {integrity: sha512-+QlQMHrmSmF++7gK0Jjgy7W5aa91gAfpFWPyY4gUwkrSc+mY+JPPYW4peAf+v6dXABOgfKH/JOaFPSv227SSTQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-string@0.2.3': + resolution: {integrity: sha512-KC2sHwnIo775uEPRG7miERHb3DMABf37jS6o2bmcMHyME+gl+HC/bnqgwYTfgOsM0YOvYMSgA1HJo6iPDXdFrw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-uint16array@0.2.3': + resolution: {integrity: sha512-gHnj9FpIpyiIgsDfA7exvfqqn91THgGJD6BAWqiBgQQccK++K5q3ARc31ysTBuIea1FqYfpy30vt2ZW46lIMcw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-uint32array@0.2.3': + resolution: {integrity: sha512-Jt9hp3iolCoQC6zC/yisTvxcuxhZ9jyiYwNzyEzgoY4GdV/WETuTpQKwTKbgQkJdUbcl/3VIQaVopEJSUaoV5w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-uint8array@0.2.3': + resolution: {integrity: sha512-a+CzS7ffk2D8oFsswqGJk3L8zpqcnzuB5rwolilRVQtrWjQT/s2ydv5b1HbMB2rEcT6h64rYJDNAITeetGmzBA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-is-uint8clampedarray@0.2.3': + resolution: {integrity: sha512-tGyJ74IqIFczWUZAAaR/xlGSzh6vO8t8SYV33ruNPjah/d69uijXHRVohx/aO3xaQ7bbVapk/WbGosmILI7sIw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-napi-equal-types@0.2.3': + resolution: {integrity: sha512-JfESREBknrUOorPQp6y0vQicG5oSrzopRs2/Uas4tXRWRWe8J+WI60W/HTNAbfow03a5k30M9b7Vg81N6zf2mw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-napi-is-type@0.2.3': + resolution: {integrity: sha512-EajA0tANjWepz0MVE0zZU45CXLA12/uuRpQ+680ZBfULsVc0plipSQfIMePBgHueJe9YMgyqm3r25RXzf98i7g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-napi-status-ok@0.2.3': + resolution: {integrity: sha512-rA+0Vo1ULn4uBHQ+FfagPAcikFT3W/t9I7HdP7SMjz71IZpkWyuxVFZi5MTv2+7hGMYA8be5FibLz20uzbZ9pA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/assert-tools-array-function@0.2.3': + resolution: {integrity: sha512-z2a2G6f9Exz/V0UfE00TLm1sp6W+pOdYltekRRgdEgFNPPP66WxaJfFLkXwqZv0X75U6TuQ0pp7tFq6Ac0lf5A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/boolean-ctor@0.2.3': + resolution: {integrity: sha512-JmVu1SdJHYK5ubLl8nqi0gfYX5kAcSRRJYNQ7JuG8oU6WtkIMsC0ahQ8+3G673czRaQ7HGE+pL0IZiH/s8HSrQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/buffer-ctor@0.2.3': + resolution: {integrity: sha512-mYODa05InvuouduT+PzGdUw1trJ5/hKY4ve2GnO+6Hm1JJV7m11uT1wpk9ewRnWI6WC9C6UXHnNBk0XQrGYbTA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/buffer-from-buffer@0.2.3': + resolution: {integrity: sha512-1EuFkFSPk3PyEQ1aLgXFC33jK9oq5ExqNhC9WkhAmhHRwonof8QkwmLiZK8oN4Zl2o4ksmCY7huSMP1UWXnjfw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/complex-float32-ctor@0.1.1': + resolution: {integrity: sha512-P5aJ7kJ3VkOCvtwIipYH2t/vENK8XnwQGDChQiZAgkJadrF2dKYOXa8t1vWEsy/AMVujAjamHlCXQffhafYm8A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/complex-float32-reim@0.1.4': + resolution: {integrity: sha512-PXnims1rRAVGARrpoeiwxkYcThjCoXv3iLhUhz8oyulcHteGkJuBAmhnrLANfkh2ro+bFlk6lC9znLt13SOM2g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/complex-float64-ctor@0.1.2': + resolution: {integrity: sha512-xCVE2Lv1/I7A/2mDloT3hDCpA9NP9XOqZKpyKhg3D/9D/vJnh1rnn9CK8+xzVfRborsyQbJF7OI+Ijc3szDI1A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/complex-float64-reim@0.1.4': + resolution: {integrity: sha512-5IxS+GqkVezTGh57Gp7pIyAHS+obDqUpq5y7KpAjrH0s6gp6oN/m+tmPh7K6Yz8yX+7FLVn5b9lZxY1otBBEJQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-array-max-typed-array-length@0.2.3': + resolution: {integrity: sha512-Npc2GG28d1+dgdb2akvzDNZZpiGTIAEe1vvJzBLfmVnDkqxYSuTu55N9jcXtTvrPOLqxwWy7K8AMAKboMFEFVA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-eps@0.2.3': + resolution: {integrity: sha512-gdDE9eYNuJqIxmpWFfFsTx24uJv3BV/S+MKi3vj/TgWFeGMnsqHP+xIf46Hby2OwRbr6yYzsVwfJMAzF7ItCQQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-exponent-bias@0.3.1': + resolution: {integrity: sha512-fUqcun9eGniI8sqsZf06qa064cKcp6cARfX9dXuGnCQWSWEwn8Q/LKr8zEgUVEu6cJCVyCMHgm8VPpkTpnFPAQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-exponent-mask@0.1.1': + resolution: {integrity: sha512-5J38bgxW+UlF9AHG+C9Z7ilx25aPSOr3ECyK0LThG9H8Kw2zL+9cNbt72vRnVr0BSCv3NKvgxPza+B/bB7IJpg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-max@0.2.3': + resolution: {integrity: sha512-M1Hgv/fUdnuV6uw9yyXXOAf7Bzf7I/8naaYolXGX4h7B6XSuCT6Of/0BRxXlU9D+V6zPniGo2zdBaXUGfek28g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-num-significand-bits@0.0.2': + resolution: {integrity: sha512-qml2Wmm1/gHOGmEfrR1w5G2wYsBHKfCLBaJ8idZ4IMcBfmR8Ez4I9K3uQ3EZ6mW8QU2nUIgp0Fc99lnzLoH+7A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-sign-mask@0.1.1': + resolution: {integrity: sha512-LpLyCmkhzwotXb1mAAkAZ4sTk5lh5lW3Q1pOp6jtepEFT/marQsoeTe+0r0mIwSncGbym/eEB455qUZQsdiTBg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-significand-mask@0.1.1': + resolution: {integrity: sha512-+IA0fHkBV7X00ZTqAo0ZZSSuh9fjLGB8+4ot8iR0irC0YxPLkvyPzFHujnWFu8UD1Eu3SZcyaCY20CLcgsGr9Q==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float16-smallest-normal@0.2.3': + resolution: {integrity: sha512-u/EzlthAeRba4TePLQzTyivIAp4GB9jLdOwE9FdxSYmqUwAbEIODkd7vft1ArzuCILRHM4NNNESJeaLlv9Qd7w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-abs-mask@0.2.3': + resolution: {integrity: sha512-owGGn8KvmHmne0u5ItTvOpHL6lUJP+tuUe2wzrKYiZuoUTg+7cOhNW4fPWCK8AFYxs11Ha3qPA7oAzjqlq+Log==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-eps@0.2.3': + resolution: {integrity: sha512-yJY7ipPOriceMh2zQvNVDYRrBJlQoBN+7cejox5DxTTn5zytyJn9sfdBQs2buGBqT7BiV+RJE7iE1vQdhuslcQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-exponent-bias@0.2.3': + resolution: {integrity: sha512-AdJFdh75o8cBwT/zjRebtZGGz0lYY2P/ITKrxHo4snqinm+DoMFRChkYM2s0QYn8ZgFBHQ1iEPB0cYzxTLk3tg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-exponent-mask@0.2.3': + resolution: {integrity: sha512-mkSAIakaixXJy45Ss8G7QbrS6eQemGjfIZhwmwlWDHxCJEVcfpuIijO/N0gtlF/MnGSOACwgn3Qkt1t3Eg30IQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-ninf@0.2.3': + resolution: {integrity: sha512-X0t2gw6UKfzDRRjYH8k8dqRv2C6iI2sn3UrunRIzCL4WYYR+QWn1UwjZWG4fviIE+QuEGTyoKKb4wG6gTiS8yw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-num-significand-bits@0.1.1': + resolution: {integrity: sha512-pb5IMIdVb22DP6yJc1PmsqTc3mmn02ze4P1oE5aZY/0v4bPUf9yegUmYIyCEWPfM1HX/xnW4GwXDZ3QptSgLqg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-pinf@0.2.3': + resolution: {integrity: sha512-ofzg0/NDAjALtXw0GPGzsfwpTEziENlSNln2PqoPphno6u7yfCD7BWblIinvDALFwbpsUVWuxAvarL8XnXl4mA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-sign-mask@0.2.3': + resolution: {integrity: sha512-CdGdK5nHesLEkS6TeQ/jdl3iThAZYrNCysRVshkpz15ORgvbFyHiXaYFIGkwt0a5RnA4i11e3nioHBNWxITGxQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float32-significand-mask@0.2.4': + resolution: {integrity: sha512-6oBi94bfEEqEPHyzgS3YNBK0fjCRsh/8A6xyaPuHBP+ltGmGfNsZmsZ5vQ+vChszKVyJG1/Bd/j4wif7GGJ/3A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float64-eps@0.2.3': + resolution: {integrity: sha512-TB4+YU9vc9RwMmSrCt9+UuFeXp+a29Q1KrzcBTcxU8uB/2totg6yYAFwhs8+48Vr5T/bQOsTQpuxXA5KEaThrg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float64-high-word-abs-mask@0.2.3': + resolution: {integrity: sha512-AXa286EisR1y71U71M18QvzldmIWsh1TNimoBjrUdCQn5jELOJAQPriTKt0H8kngfOUYfy/+EJwCpuQEL/VWrg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float64-ninf@0.2.3': + resolution: {integrity: sha512-1yBmgdNkvxWR0IfebDnhS4KOHPNgTpe3j6CCRt3qUW98l45tUkjuqyKKu8jNvjJCq+qKaNHNNk8xAl+/eDuABg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-float64-pinf@0.2.3': + resolution: {integrity: sha512-8kRy0XOvW7QiJlxdy8MXx7rM3S4H/ZsUI+q9dNoarKVDBtWzkqnEG/u5QYouVjGHaSVL8C7b7qwL1FdpSD+24g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int16-max@0.2.3': + resolution: {integrity: sha512-mPHYy0jMD8ALGd5QDOAYU8mO57sbYsivYUVZq7+/mJDXldVprD+fUJFX8QOkuRW1LIsVSqHA1F4oGrnjFM+lVA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int16-min@0.2.3': + resolution: {integrity: sha512-FZrIJecQGw5qEYSrd4inQgx9xe5igDb+UmZZDKccv39xwWpPPfprt7Sr+zholUdcGGpoJdftBI9QbbLQRTOqcw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int32-max@0.3.1': + resolution: {integrity: sha512-ZAhWfNaBhVzDatkha1q0vu7tO6XRbP6kxJk/Erh4emgE/4JpC9WPlXfkZvhOoCWQ1Rsll7tevxN3rfu6oGN7OA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int32-min@0.2.3': + resolution: {integrity: sha512-xXa5t+tlRuwi+cHru4p8gFPnXqa2hI1bn+K6IoTd8Pj+ySKTkR0evSgNIrO6kcEf+kj2YuNXZZM3sqK33SH9vw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int8-max@0.2.3': + resolution: {integrity: sha512-TD5BzgiZshK/LD/3Ya4fp2mI0j/XGm+XH7TOWLjMWrH3JYbW/GIQfTTDsXTEu52Te0M64Xy5WBmvrXoObFkqSw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-int8-min@0.2.3': + resolution: {integrity: sha512-sD5yCZIujDG0DKLBSj+r3UzxQ04yTyQ4yT/umQrtTHMHAGmZnNZ0Jab8VmxBxfZUC2PAVc841hS/XKQglw5Fsg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-uint16-max@0.2.3': + resolution: {integrity: sha512-QLqt85JMBnL8ozliswgAeR9oWrsmWeaeWjr10P0zbGV1dKhM2HoRK3VXjO4SCtNWUwiV4w8Wa5ylSYdJhORDjA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-uint32-max@0.2.3': + resolution: {integrity: sha512-NYQIGMA3Z8+1gp2qUfmr90whvZJLDq737ipNeujrFm98/RcbBlCRmXCLlj0WMwxniw2T35ysfclAcFSE7caUCA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/constants-uint8-max@0.2.3': + resolution: {integrity: sha512-B4VzHho0T43dJk3m1x9V3GZ1OOK9AKNojo3jOweixBLLlofaEIe0E9f4Gihc2ef7+M4azYptsJdPY1jPy7kK2g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/error-tools-fmtprodmsg@0.2.3': + resolution: {integrity: sha512-mt4YRp52oCkNWhxkUIJkeGaFnSvZkyfl4rOJX8Drz99xFiIdanXcSacckhrXXu6NQvMDKidCGr6DvrhHHx15LQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/fs-exists@0.2.3': + resolution: {integrity: sha512-kfHok9sKxvCftgWriVtNjqezNZA72Q/rJh78lcrGCLPvSHGIAL460olzl7D1OgAgQvM3Cwdoupay0o3dBCCwVQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/fs-resolve-parent-path@0.2.3': + resolution: {integrity: sha512-bof7pQQewv8LLaSi2Ut77mkEv4v45Z/w/hYocG0UUzmXhu4lXIiCcXz9ZAMJC4tdN5hwBReJGfJ0mQryddhGGA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-assert-is-finite@0.2.3': + resolution: {integrity: sha512-PqqxkiubjHe9jdIfAf/PTHZLbYIk1wcFwPGALM+PR3kjRngEkti7NbbkTQX3sByZCCfRwOf7VxLPWDlGo3EmLw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-assert-is-finitef@0.2.3': + resolution: {integrity: sha512-FiP4w+PjwD6zapXgrD0hLEtnUMi+uCcBQw+tU9cAGyB+7NrhnUoKSWgHsjtLGLMD08gWsFOIjZzZRavx4PocfA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-assert-is-integer@0.2.7': + resolution: {integrity: sha512-rsxiM7N9Y5kd9elTEJpx1kAcHBeHETH46vn2s5FoKyNt7oZLqv9iE5oKF3oNznA3H47o7SNyQSdcJpUDqS1W3w==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-assert-is-nan@0.2.3': + resolution: {integrity: sha512-mj1p+JUSfbsiBI3+hwRV7bS0qgI2nWdzrFtBJUSLk+17MJSdA4aboUsXM12wIV1kg5JNfEAn+5eh4NF/sMvaoQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-assert-is-nanf@0.2.3': + resolution: {integrity: sha512-sWMMFhTrkeYu/t5bv7KZ8Rm7H4pD+O1+wYYNu1CsJ4y1Q6HwSDeHBNIH7Z4zHOOSFiOidb04jnqMGSsL5UCFRg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-napi-unary@0.2.7': + resolution: {integrity: sha512-eEu0nAq8WzCGe15ppGJWtLGhKOoG4OcMJQpsfbjwaa3sZ9h1sREmV1iiAFWyi/mHy8C0y6PiG2szx4D/F0ywmg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-special-abs@0.2.3': + resolution: {integrity: sha512-0n2Jb1NQv/siPL5EJFf6/cCuK63zqgqFjQwPujJI8B+dDizlHJKF4jR6yOtavrB5RvVf37/3Ibpdjf8UPf3uzA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-special-absf@0.2.3': + resolution: {integrity: sha512-lUEEnIo6jpR397bn4mphlihPBMncdgKYQQYZ8nwnL/wGNE9InwqLqrO6otM+CO2nkG/kOyt0dSAhSSffqBZU6g==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/math-base-special-floor@0.2.4': + resolution: {integrity: sha512-YSDt9gHSp8SeVBVzHfpZZ1HFEQDPihHUF92zTDCRGKeKBC4BTFlw9Q4/fNWqS1mtuBGli1zgpRbiTv3atQ5EjQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-argv-double@0.2.2': + resolution: {integrity: sha512-QJrnt4CTS8x7WHny4s8eHvGT2u5x8BSH6RWVaPg6ZrnHwqz9Jz48JGTawFKLgcyvZyeE9kCwKDkPIN7MbXcHKQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-argv-float@0.2.3': + resolution: {integrity: sha512-2hNFgenY7M5iVfIbRw+xccutxtKfk89VQb1RMGbSAOrRcR53J5OENAaddohrCp1zT1FKe14LCG/k+LlZR5jEdA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-argv-int32@0.2.3': + resolution: {integrity: sha512-Set5AdSIUePU/Y0gT+WC4lerzahwaAQJd1USCgnKQD55xmDfXAPMyJliZnZPNUyjQ6SsWN6PaiOfJluq+oVcVQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-argv@0.2.3': + resolution: {integrity: sha512-JQe8cqcCmxVhAMZ6z7Ysx0hmY8DtIGEXcM7Ymmjxl5VNaVXq/grw3HEQm5Fcb+TdqJIb7xEbsRHCiowRzvk0Pg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-create-int32@0.0.3': + resolution: {integrity: sha512-tIT9BWA3jmOjzz4XTwVplkKETI3dyTK4BZoMtbQGnTxfsHz+pNdgepbp8hEx7wJADHKO2L8YTqddTHij4H3Ptw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/napi-export@0.3.1': + resolution: {integrity: sha512-JQaR2FYyIGprLgjg02JwgGGbjaYgRsWJpF72T748AiUQMd8SEN/mjUkqPq4zYLRjYATpKxnDUQNvP642z9JnIg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-ctor@0.2.3': + resolution: {integrity: sha512-T4xeLGny/gBRe4ZJcS5ugluT8E7Y/LCEHycTQingYWqbBK+5XJED7zJrcawkneewXd7CgqypCtVuI0BOxBBzcQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float16-base-to-float32@0.1.2': + resolution: {integrity: sha512-OClMQYz6GJEiSxrzhNp2LK4K8j75Rq6HCk+ReRBNQjXYdvNfzzaYoqnv3ZSzrXdvqUoGz+19XaVTLLzkKT1GCg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float16-base-to-float64@0.1.2': + resolution: {integrity: sha512-1MNdMfThDFEIMeJHKmum343x9sbiRplRmsjEHojVGyMQNuxOamzbzw+Gw3xl04GrfOIUG673KZEljwUwDFgtUA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float16-ctor@0.1.2': + resolution: {integrity: sha512-bdBhgwgtRClxBU9Ef8puY/WQ+DMqUr0oXfkfCrYKUATmqVQghv3bDw/dMggz01AguJo2ZtdjGp3PjQwNZlmHcg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float32-base-exponent@0.2.4': + resolution: {integrity: sha512-JUuSZCPD6yWA2jJhtS5MIYSZGerJJCXfwStz0Mqsh76x166mSkiZ4dH3VpDz2OMrZk6rvt35UB2HhAZecTqFAw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float32-base-to-float16@0.1.1': + resolution: {integrity: sha512-phYWBtfheX0c/pXtbiK/xr6lVwU5HKYVNgmol8y0HE8NvwXG3oCXLySa2p0jr+Xi7G9xtMdXW4Eq0rShs4lTmQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float32-base-to-word@0.2.3': + resolution: {integrity: sha512-E42KOQ+jqVDElwvFtrDMEkXYl03btIPan3g/YAScCibfmYDkYRDT9iuaydyYS/O3+m/MPM1Jhn+U/B8+f+C1HQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float64-base-to-float16@0.1.2': + resolution: {integrity: sha512-d+ZJYGoykrzI3H0GRCI3qqyjspgNcIvECQ4OjyC1qdy1haT3cgoj1rwWkJS9uFKD/QgWlFGMrcmRzf0MVj17zg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float64-base-to-float32@0.2.3': + resolution: {integrity: sha512-+Lu8vk1oLCxO8RaRzw8pEY+BP2TWROweNFiML0w7TP4i9ZyG5E6EJdxZ2J3c29LVPfsf+oLOOxyySHH2kjwIsw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/number-float64-base-to-words@0.2.3': + resolution: {integrity: sha512-G63YuysfTKRP3Tkn19zBxTseS+uYnnOYZSN9VRTWrx5iNE8IYDzanAJIolSBZ7PuFMoE+HmjEZ0E9SX3NsLKvg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/object-ctor@0.2.2': + resolution: {integrity: sha512-3zkUkzZ9LdX+J5fEyU/D7I2c+0qG+ejNI3tCtSFek35bezdqWs9tOAAUiU6HeQJcGgqOmkakB4apIxvN+eehCA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/os-byte-order@0.2.3': + resolution: {integrity: sha512-QU4JY7WN4inoLVz+ML92TOevgyxZa+d2RcA1EmaDC0y0TNrXez1GmIn1t7+L7UTJEdf39IUm6VfdMmYdcDwFTw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/os-float-word-order@0.2.3': + resolution: {integrity: sha512-5dLZBCNn9Fge3kq7uHIh7S1qshfe5L7pjLYLSUGxxKa7gecAbsaQBPLxDkFiFwMytr6tnGGjWQfyVbhq/+RjlQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/process-cwd@0.2.3': + resolution: {integrity: sha512-T/sYVJjs9iHAOK9B8yGymGY+59eAz20G7eL/G4cObT6sWssAF7s/2+KgzAvWB0maE9qX6qnP4zBg2msCsWwvVg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/regexp-extended-length-path@0.2.3': + resolution: {integrity: sha512-3HYXiSzBpKz4nSOCtrYtMw9/OXN4EmTvq0owXv6HMvvDweFskieBcdNlVcOl0ViezjvUuvUL0DHJHdGp6SQEIw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/regexp-function-name@0.2.3': + resolution: {integrity: sha512-ER1C2rUW5Kzu5w4W0gbrJdIj5STNO5RfNy4GeiqycezeiXT99G393QeN4irqcOItYkeZhwbpY2ZzwlLxQvod1A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/regexp-regexp@0.2.3': + resolution: {integrity: sha512-TO2w4kWejewUZ0xAh+VW7wM1lZwMi2KhKcJ+Jpqv7nFIPNwY8+kghjRIeDU2sp1Mmk7fHZWJ9xeEilg5NUyhnw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-base-format-interpolate@0.2.4': + resolution: {integrity: sha512-POG725+DPEmzEJbMFTFPdKM4vA5i+t7juNYs+H2cQz5JXiPV1+5+P3epnO+/DqHWzLhiMlsKDCZq03C3WcBkSA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-base-format-tokenize@0.2.4': + resolution: {integrity: sha512-qYHSmqFv7vloLFvjBAbZCn5qtxj7M+Qru3VqBooJTxWMcUeZOEBZg18/PFgfUrZhji5MVWDXbRTAx5Bu5M5nQA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-base-lowercase@0.4.1': + resolution: {integrity: sha512-3NcDy2j6HtvKrC2GRCt5mKiYaWWHLLSQRSbuu28WEzA98/2izmiHYkR0i9IeEd4Tqrxqof/FXP4gWj1f14RXTQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-base-replace@0.2.3': + resolution: {integrity: sha512-FJdh2GzIkgMlr0v/ZZKD5cWRs+SMOhBTWxAexmWoYFL3WL1YMDgwI5hRtYh/ySyOy94MjJmYwRMpIVdxeclrAw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-format@0.2.3': + resolution: {integrity: sha512-uE0LHUUnWKfxFeipyfb9yWbs+iczz6dHaolSGC1WBzMVyeHqx8xvq+yP3f2POYWgEcknmxNgU21WSqToSNrxdA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/string-replace@0.2.3': + resolution: {integrity: sha512-CXJhl/L2TfRrkVUNVp8OfNsKeuUzOZXzJLUDoopHtQalbWBww1fDSDj1fwGpzgZV4IODs8LCsiIxLBnLRP6emg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/symbol-ctor@0.2.3': + resolution: {integrity: sha512-wnuFrxnmhg2p6QQP0B/j97LD5ys2d1bHuwVvh9yuUSCLoX/GankaVyGSufk2ROlsxrcm2HPhSDB/ILBHeUmamA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/symbol-to-primitive@0.1.2': + resolution: {integrity: sha512-//r52ighL35HprZrK5Pdiz1L27l47X9o+0LOJ84mTk5eB89i9shVRjOpHauKyx/uy9VkDoxUkMY0PwPrAIp4aw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/types@0.4.3': + resolution: {integrity: sha512-9GCqS2eni2VSwa5/CCniAJ4I1eWLtvior1z6hoPJp6VTNMgW9Lh4BiI84IO0xun/0qAclJ+mTGTiCTZxZLK13A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-constructor-name@0.2.3': + resolution: {integrity: sha512-/ApogDUIFxndcmnEeI6ctsqWt66c40g1T7R0nS9aOoYgnDOdmpUFSflzf++MDWksBycGsqgm1UD7IU54m4u7jg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-convert-path@0.2.3': + resolution: {integrity: sha512-fmqJ6Qvce0HV7D+uYrram/+iAHB3u2gMys9zeh0uc/h411sB8Hw9X7i1w2cJevCpouMzqTnpzp75C+swkoQlKA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-copy@0.2.3': + resolution: {integrity: sha512-Fc91OpBuHZAZpWO/8ZZQTGaCvaMX3KZe2cVD5eMZjYQ88/PGVl/y/zY5KsKbc7A46/IbDw2GD757ii0Bv+zNlg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-define-nonenumerable-read-only-property@0.2.3': + resolution: {integrity: sha512-W0rqnRsXgW3GjcwEcOMi0mI/CXn3TVqmFvcxItLUZPJNYmwrU6+t+9kGldhMw845DyOtv8uYASrnNoVE/VfiDw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-define-property@0.2.5': + resolution: {integrity: sha512-WpbXc2uq8vtSqmWFzVrFifNj6HGoGHIZHolxX5GfRFbj4RNme1u/wtklmiOvcHE0s6SwPX764tuFXIgHBR5Vag==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-define-read-only-property@0.2.3': + resolution: {integrity: sha512-3sGMpmmSlErHup+Y6YwIgN075GW3t1FD8ARS+zHaU1U1IKEaiOVKMH49WfBGiSsVfV7sfieoZC5B//MrSWQjaA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-escape-regexp-string@0.2.3': + resolution: {integrity: sha512-ZQAq5HHF+C1fmK+RvqeMO0iIQf5kGl2Ofx9LYloURUPZD2zVMX+2hxDLFBTPAusqYxffQAjj3ap4LE/J0+YiRQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-get-prototype-of@0.2.3': + resolution: {integrity: sha512-unEoKhhAnV4AbQ87oPo1v+4xLjd00nQxJLSvl43PkdoBn/GKNv/B6SMnQWmM5b62esWTbxBu/uWCYoglbprxkA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-global@0.2.3': + resolution: {integrity: sha512-e8gyuENhSeF8udH7vlZMDbNVQ/ZxZ4vWOcpB2iw06z1HSFZWc31Coh19ScfgagLfofo2GQYPJI3tJtZQYsUS6A==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-index-of@0.2.3': + resolution: {integrity: sha512-R5lBztlvTWiH0CFFSYsSDGDexQH5y9UVIAHNevp/uS3jE0+nMHREWEkIwCHxkAsYlbi1Dlbyq8+vapswxgBSNw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-keys@0.2.3': + resolution: {integrity: sha512-pH7orA2LGYVQ/uTDtNFPocdpF8AfiZIcJpGR1178OhoaMv7lXa4avVHCNY5TT+GHZebJxIWJum1zmi8onYuVwQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-library-manifest@0.2.4': + resolution: {integrity: sha512-sNzEclCbpRRa35DBv8ZOcOjCesBLczkOFX8o7mMrG9VWhyZuJUCBtgSPmzGIIBLAsVEYJuguEk4iYoMqFnCwnw==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-native-class@0.2.3': + resolution: {integrity: sha512-UGXCPhgmOjwMpEQ5fFiifcqrhUnP8ICyWzEkHphUrh2/0pHyK0zilZvcODfROgzoy6L9ivUUuR6NjrAI+j14hA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-noop@0.2.3': + resolution: {integrity: sha512-kZQWwUwq+RwmKBZZD+6dnRwhjfut5fBgbEappugvVjFGp12wDKXCj6D6Y/xTmBdKFRumO8/gzkj9n6DKNDchpA==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-property-descriptor@0.2.3': + resolution: {integrity: sha512-JAEFCe7Co791tp7G/Lgu3QfRx+IPBDFYeWDaggtM+GfMZ5KfSH9Zxgo2QGcvO6QRlDCKJajLdMxJxkyjGQ9EaQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-property-names@0.2.3': + resolution: {integrity: sha512-1Kx/nbGIDSCNmN25DnJN1F1Qdlq+udMqtDn/ZwIeM/vzeQknooUSmeRAuux4chNy0PLrmzLe4gm+NQ0H50kxQg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-regexp-from-string@0.2.3': + resolution: {integrity: sha512-BJbV/rb7quf30JWMAdPLfemnY/omaMwJ5AVWEmWvzVPFuL5vVsL+i1NnyR24saiR/AEh0R5WKRcr3ytC+GuiFg==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@stdlib/utils-type-of@0.2.3': + resolution: {integrity: sha512-2KmDUfqIv0g4lrWtnyR5xw82NvdsEwUHON43JAe55XRlLWnk1V1troiyJiM9fccENtl524kVRBnD7RMohmUyyQ==} + engines: {node: '>=0.10.0', npm: '>2.7.0'} + os: [aix, darwin, freebsd, linux, macos, openbsd, sunos, win32, windows] + + '@storybook/addon-actions@8.6.14': + resolution: {integrity: sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-backgrounds@8.6.14': + resolution: {integrity: sha512-l9xS8qWe5n4tvMwth09QxH2PmJbCctEvBAc1tjjRasAfrd69f7/uFK4WhwJAstzBTNgTc8VXI4w8ZR97i1sFbg==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-controls@8.6.14': + resolution: {integrity: sha512-IiQpkNJdiRyA4Mq9mzjZlvQugL/aE7hNgVxBBGPiIZG6wb6Ht9hNnBYpap5ZXXFKV9p2qVI0FZK445ONmAa+Cw==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-docs@10.3.3': + resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} + peerDependencies: + storybook: ^10.3.3 + + '@storybook/addon-docs@8.6.14': + resolution: {integrity: sha512-Obpd0OhAF99JyU5pp5ci17YmpcQtMNgqW2pTXV8jAiiipWpwO++hNDeQmLmlSXB399XjtRDOcDVkoc7rc6JzdQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-essentials@8.6.14': + resolution: {integrity: sha512-5ZZSHNaW9mXMOFkoPyc3QkoNGdJHETZydI62/OASR0lmPlJ1065TNigEo5dJddmZNn0/3bkE8eKMAzLnO5eIdA==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-highlight@8.6.14': + resolution: {integrity: sha512-4H19OJlapkofiE9tM6K/vsepf4ir9jMm9T+zw5L85blJZxhKZIbJ6FO0TCG9PDc4iPt3L6+aq5B0X29s9zicNQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-interactions@8.6.14': + resolution: {integrity: sha512-8VmElhm2XOjh22l/dO4UmXxNOolGhNiSpBcls2pqWSraVh4a670EyYBZsHpkXqfNHo2YgKyZN3C91+9zfH79qQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-links@10.3.3': + resolution: {integrity: sha512-tazBHlB+YbU62bde5DWsq0lnxZjcAsPB3YRUpN2hSMfAySsudRingyWrgu5KeOxXhJvKJj0ohjQvGcMx/wgQUA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + peerDependenciesMeta: + react: + optional: true + + '@storybook/addon-links@8.6.14': + resolution: {integrity: sha512-DRlXHIyZzOruAZkxmXfVgTF+4d6K27pFcH4cUsm3KT1AXuZbr23lb5iZHpUZoG6lmU85Sru4xCEgewSTXBIe1w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.6.14 + peerDependenciesMeta: + react: + optional: true + + '@storybook/addon-measure@8.6.14': + resolution: {integrity: sha512-1Tlyb72NX8aAqm6I6OICsUuGOP6hgnXcuFlXucyhKomPa6j3Eu2vKu561t/f0oGtAK2nO93Z70kVaEh5X+vaGw==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-outline@8.6.14': + resolution: {integrity: sha512-CW857JvN6OxGWElqjlzJO2S69DHf+xO3WsEfT5mT3ZtIjmsvRDukdWfDU9bIYUFyA2lFvYjncBGjbK+I91XR7w==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-styling@1.3.7': + resolution: {integrity: sha512-JSBZMOrSw/3rlq5YoEI7Qyq703KSNP0Jd+gxTWu3/tP6245mpjn2dXnR8FvqVxCi+FG4lt2kQyPzgsuwEw1SSA==} + deprecated: 'This package has been split into @storybook/addon-styling-webpack and @storybook/addon-themes. More info: https://github.com/storybookjs/addon-styling' + hasBin: true + peerDependencies: + less: ^3.5.0 || ^4.0.0 + postcss: ^7.0.0 || ^8.0.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: ^5.0.0 + peerDependenciesMeta: + less: + optional: true + postcss: + optional: true + react: + optional: true + react-dom: + optional: true + webpack: + optional: true + + '@storybook/addon-toolbars@8.6.14': + resolution: {integrity: sha512-W/wEXT8h3VyZTVfWK/84BAcjAxTdtRiAkT2KAN0nbSHxxB5KEM1MjKpKu2upyzzMa3EywITqbfy4dP6lpkVTwQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/addon-viewport@8.6.14': + resolution: {integrity: sha512-gNzVQbMqRC+/4uQTPI2ZrWuRHGquTMZpdgB9DrD88VTEjNudP+J6r8myLfr2VvGksBbUMHkGHMXHuIhrBEnXYA==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/api@7.6.17': + resolution: {integrity: sha512-l92PI+5XL4zB/o4IBWFCKQWTXvPg9hR45DCJqlPHrLZStiR6Xj1mbrtOjUlgIOH+nYb/SZFZqO53hhrs7X4Nvg==} + + '@storybook/blocks@8.6.14': + resolution: {integrity: sha512-rBMHAfA39AGHgkrDze4RmsnQTMw1ND5fGWobr9pDcJdnDKWQWNRD7Nrlxj0gFlN3n4D9lEZhWGdFrCbku7FVAQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^8.6.14 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@storybook/builder-vite@10.3.3': + resolution: {integrity: sha512-awspKCTZvXyeV3KabL0id62mFbxR5u/5yyGQultwCiSb2/yVgBfip2MAqLyS850pvTiB6QFVM9deOyd2/G/bEA==} + peerDependencies: + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/builder-vite@8.6.14': + resolution: {integrity: sha512-ajWYhy32ksBWxwWHrjwZzyC0Ii5ZTeu5lsqA95Q/EQBB0P5qWlHWGM3AVyv82Mz/ND03ebGy123uVwgf6olnYQ==} + peerDependencies: + storybook: ^8.6.14 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + + '@storybook/channels@7.6.17': + resolution: {integrity: sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==} + + '@storybook/channels@7.6.24': + resolution: {integrity: sha512-rNSifUbCjUPWQMZPptY5VTY4c4iOrCzDKmmDeBeurPH0ZiDvnJjW7v9dlXzlDNoXFUv+jBE+RjrEfNWsnJhvsQ==} + + '@storybook/client-logger@7.6.17': + resolution: {integrity: sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==} + + '@storybook/client-logger@7.6.24': + resolution: {integrity: sha512-Xgn62FLhTzGJFl/uAMukJrfqAhiInkJ91ZwZMqEl8bdgeGO6ISkijDqQebqI0KyqB4ZpD11jVvEOQ/TowLebZw==} + + '@storybook/components@7.6.24': + resolution: {integrity: sha512-yjkJXr3kYGYyMNV/IuOI220ZVAH1z+wovZfsW4h0F56hTc0pFAhC551C8hk2AvJjSAa7N4VPm42zAqNb0HNTmA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/components@8.6.14': + resolution: {integrity: sha512-HNR2mC5I4Z5ek8kTrVZlIY/B8gJGs5b3XdZPBPBopTIN6U/YHXiDyOjY3JlaS4fSG1fVhp/Qp1TpMn1w/9m1pw==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/core-common@7.6.24': + resolution: {integrity: sha512-wgCarEWFodQaJ76uLxwHoxtGwQPymzoZHFLE2fJm04y6sdotUgattUUY5FxbhSveImj6VTLDDzstkzxxz166UQ==} + + '@storybook/core-events@7.6.17': + resolution: {integrity: sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==} + + '@storybook/core-events@7.6.24': + resolution: {integrity: sha512-9mhV2grn+IYljRJSqoTec3XhoMs1Va0aYWe937siX3Fj77F6zuXmEugrJstgVYsPAgcqH9eBSCM7rwdmbo7LVg==} + + '@storybook/core@8.6.15': + resolution: {integrity: sha512-VFpKcphNurJpSC4fpUfKL3GTXVoL53oytghGR30QIw5jKWwaT50HVbTyb41BLOUuZjmMhUQA8weiQEew6RX0gw==} + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + '@storybook/csf-plugin@10.3.3': + resolution: {integrity: sha512-Utlh7zubm+4iOzBBfzLW4F4vD99UBtl2Do4edlzK2F7krQIcFvR2ontjAE8S1FQVLZAC3WHalCOS+Ch8zf3knA==} + peerDependencies: + esbuild: '*' + rollup: '*' + storybook: ^10.3.3 + vite: '*' + webpack: '*' + peerDependenciesMeta: + esbuild: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + '@storybook/csf-plugin@8.6.14': + resolution: {integrity: sha512-dErtc9teAuN+eelN8FojzFE635xlq9cNGGGEu0WEmMUQ4iJ8pingvBO1N8X3scz4Ry7KnxX++NNf3J3gpxS8qQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/csf@0.1.13': + resolution: {integrity: sha512-7xOOwCLGB3ebM87eemep89MYRFTko+D8qE7EdAAq74lgdqRR5cOUtYWJLjO2dLtP94nqoOdHJo6MdLLKzg412Q==} + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@1.6.0': + resolution: {integrity: sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + + '@storybook/icons@2.0.1': + resolution: {integrity: sha512-/smVjw88yK3CKsiuR71vNgWQ9+NuY2L+e8X7IMrFjexjm6ZR8ULrV2DRkTA61aV6ryefslzHEGDInGpnNeIocg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@storybook/instrumenter@8.6.14': + resolution: {integrity: sha512-iG4MlWCcz1L7Yu8AwgsnfVAmMbvyRSk700Mfy2g4c8y5O+Cv1ejshE1LBBsCwHgkuqU0H4R0qu4g23+6UnUemQ==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/manager-api@7.6.17': + resolution: {integrity: sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==} + + '@storybook/manager-api@7.6.24': + resolution: {integrity: sha512-afscdt9zc8wx+s0VzIlvU+pc5X6KTGHOUj6sLlJDCzzrRG2MBNdSfwOuivlC93jV+yPdgdgW46jaK4meC9jLdg==} + + '@storybook/manager-api@8.6.14': + resolution: {integrity: sha512-ez0Zihuy17udLbfHZQXkGqwtep0mSGgHcNzGN7iZrMP1m+VmNo+7aGCJJdvXi7+iU3yq8weXSQFWg5DqWgLS7g==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/node-logger@7.6.24': + resolution: {integrity: sha512-6+kuX0q4VH1Orf0Yda+dj6svMIjtN5FbXU9lgKWbO5OY2xeyEtr/+3phxfTnfd6N89kYxDx8JGeP5ldzx2alxg==} + + '@storybook/preview-api@7.6.24': + resolution: {integrity: sha512-dBoHQeZk4ZdfeIZzc798Bl2wF0tjiY6fhl7QllBUIFqxvHTCM3YFa2vAIifr2bnxeTpvheKFhqNnOivJbSwTXQ==} + + '@storybook/preview-api@8.6.14': + resolution: {integrity: sha512-2GhcCd4dNMrnD7eooEfvbfL4I83qAqEyO0CO7JQAmIO6Rxb9BsOLLI/GD5HkvQB73ArTJ+PT50rfaO820IExOQ==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/react-dom-shim@10.3.3': + resolution: {integrity: sha512-lkhuh4G3UTreU9M3Iz5Dt32c6U+l/4XuvqLtbe1sDHENZH6aPj7y0b5FwnfHyvuTvYRhtbo29xZrF5Bp9kCC0w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + + '@storybook/react-dom-shim@8.6.14': + resolution: {integrity: sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.6.14 + + '@storybook/react-vite@10.3.3': + resolution: {integrity: sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@storybook/react-vite@8.6.14': + resolution: {integrity: sha512-FZU0xMPxa4/TO87FgcWwappOxLBHZV5HSRK5K+2bJD7rFJAoNorbHvB4Q1zvIAk7eCMjkr2GPCPHx9PRB9vJFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@storybook/test': 8.6.14 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.6.14 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + '@storybook/test': + optional: true + + '@storybook/react@10.3.3': + resolution: {integrity: sha512-cGG5TbR8Tdx9zwlpsWyBEfWrejm5iWdYF26EwIhwuKq9GFUTAVrQzo0Rs7Tqc3ZyVhRS/YfsRiWSEH+zmq2JiQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + storybook: ^10.3.3 + typescript: '>= 4.9.x' + peerDependenciesMeta: + typescript: + optional: true + + '@storybook/react@8.6.14': + resolution: {integrity: sha512-BOepx5bBFwl/CPI+F+LnmMmsG1wQYmrX/UQXgUbHQUU9Tj7E2ndTnNbpIuSLc8IrM03ru+DfwSg1Co3cxWtT+g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@storybook/test': 8.6.14 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.6.14 + typescript: '>= 4.2.x' + peerDependenciesMeta: + '@storybook/test': + optional: true + typescript: + optional: true + + '@storybook/router@7.6.17': + resolution: {integrity: sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==} + + '@storybook/router@7.6.24': + resolution: {integrity: sha512-298nfeJrcw/5o30obLxnu8YA5Mp566GIQTR1bvjglh9b2w4hJXwhGcZD8/rxrMbi7yDemGgLyyicMNvWr+cpQA==} + + '@storybook/test@8.6.14': + resolution: {integrity: sha512-GkPNBbbZmz+XRdrhMtkxPotCLOQ1BaGNp/gFZYdGDk2KmUWBKmvc5JxxOhtoXM2703IzNFlQHSSNnhrDZYuLlw==} + peerDependencies: + storybook: ^8.6.14 + + '@storybook/testing-library@0.2.2': + resolution: {integrity: sha512-L8sXFJUHmrlyU2BsWWZGuAjv39Jl1uAqUHdxmN42JY15M4+XCMjGlArdCCjDe1wpTSW6USYISA9axjZojgtvnw==} + deprecated: In Storybook 8, this package functionality has been integrated to a new package called @storybook/test, which uses Vitest APIs for an improved experience. When upgrading to Storybook 8 with 'npx storybook@latest upgrade', you will get prompted and will get an automigration for the new package. Please migrate when you can. + + '@storybook/theming@7.6.17': + resolution: {integrity: sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/theming@7.6.24': + resolution: {integrity: sha512-HuH7fkscjq5+qJTTWEY0xRZ9CMaBFpk+NdevA0eHCcBlo4yhMWb1PTUw9PHch4EIU7ng7l9tEPEaxLQT4vFUPg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/theming@8.6.14': + resolution: {integrity: sha512-r4y+LsiB37V5hzpQo+BM10PaCsp7YlZ0YcZzQP1OCkPlYXmUAFy2VvDKaFRpD8IeNPKug2u4iFm/laDEbs03dg==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/theming@8.6.15': + resolution: {integrity: sha512-dAbL0XOekyT6XsF49R6Etj3WxQ/LpdJDIswUUeHgVJ6/yd2opZOGbPxnwA3zlmAh1c0tvpPyhSDXxSG79u8e4Q==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/types@7.6.17': + resolution: {integrity: sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==} + + '@storybook/types@7.6.24': + resolution: {integrity: sha512-XOhLmXnQprLRIs4dT9kmWHgETEiGdOjbJ9ULQGoKR72wia47Buzrjwg5Ym3BTQEzrtLpo/8FD3NS+Migldp+XA==} + + '@svg-maps/world@1.0.1': + resolution: {integrity: sha512-Mawh/jEYBBHnug9S17PyePLYKJ+Xd0Bbh96mCePebpbvcbJu5YKpfKhpyMeLFmmdWPrSFxl0f0MTsJfXU0gSaQ==} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@swc/core-darwin-arm64@1.15.21': + resolution: {integrity: sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.21': + resolution: {integrity: sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.21': + resolution: {integrity: sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.21': + resolution: {integrity: sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.21': + resolution: {integrity: sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.21': + resolution: {integrity: sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.21': + resolution: {integrity: sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.21': + resolution: {integrity: sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.21': + resolution: {integrity: sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.21': + resolution: {integrity: sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.21': + resolution: {integrity: sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.21': + resolution: {integrity: sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.21': + resolution: {integrity: sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tailwindcss/line-clamp@0.4.4': + resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==} + peerDependencies: + tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.2.1': + resolution: {integrity: sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/query-core@4.36.1': + resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==} + + '@tanstack/react-query@4.36.1': + resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-virtual@3.13.23': + resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + + '@tanstack/virtual-core@3.13.23': + resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + + '@testing-library/dom@9.3.4': + resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} + engines: {node: '>=14'} + + '@testing-library/jest-dom@5.17.0': + resolution: {integrity: sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react-hooks@8.0.1': + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + + '@testing-library/react@12.1.5': + resolution: {integrity: sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==} + engines: {node: '>=12'} + peerDependencies: + react: <18.0.0 + react-dom: <18.0.0 + + '@testing-library/react@14.3.1': + resolution: {integrity: sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==} + engines: {node: '>=14'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tinybirdco/charts@0.2.4': + resolution: {integrity: sha512-0N8fYPY8uxO5KcBkW5Dgmapj4AqcWgyCmWnZBYsugXnnbx4/yg//f3/9oMWnM3RZ6ltt9moltYIzF/95NO2zNg==} + peerDependencies: + react: ^18.0.0 + react-dom: '>=16.6.0' + + '@tiptap/core@2.26.3': + resolution: {integrity: sha512-TaOJzu2v5ufsOx+yu94NqXE504zmupVdFCxH1g3hk5fzZ3gT57Lh9R/27OjwM4e6o+Z3DXDl8yfFMHIcR3zUkg==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.26.3': + resolution: {integrity: sha512-brz8+wh03TuMevNUztTSC9BzZEsLCNakPJCCicD8FRpBJoLj4benT6T3GYVdMhkk4BmhpruSFZB0FPY+rxCVlA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.27.2': + resolution: {integrity: sha512-VkwlCOcr0abTBGzjPXklJ92FCowG7InU8+Od9FyApdLNmn0utRYGRhw0Zno6VgE9EYr1JY4BRnuSa5f9wlR72w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-document@2.26.3': + resolution: {integrity: sha512-gcJg4Otchilr4eSUwhPNwbhPUkEYvXhkUZ/1MAhVGD40Ovq2P8ZWkJipA3tKOCJinL5MJK59ccZBstnKSTw+JA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-floating-menu@2.27.2': + resolution: {integrity: sha512-GUN6gPIGXS7ngRJOwdSmtBRBDt9Kt9CM/9pSwKebhLJ+honFoNA+Y6IpVyDvvDMdVNgBchiJLs6qA5H97gAePQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.26.3': + resolution: {integrity: sha512-KJWUi+2KOZejVRb2KI0NM3LgCpNimxcunbOCKsZKygV/UByzhUl7UaCAIa+ySMM+kbu/Ec3hkTzafGfaU9ZkLg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-link@2.26.3': + resolution: {integrity: sha512-cNYqAeiaG/65ctVEUOHt1MQnTF1JcdZqBkN9pLf3grzcmkmdr3w1/JbKOphZc84vOB2rxuhGZx9NFV2lrC5Qwg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-paragraph@2.26.3': + resolution: {integrity: sha512-eBC5UsaTJRUMhePtK1dcCAfes0CpqqFiewpIM0lWk4XMtpG2aoczVVVkImybbFKfqsvEEo3vgHJ2YiE5YZFCSg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-placeholder@2.26.3': + resolution: {integrity: sha512-HDF4FZj8CmQQvbSyXb/G+Ujqoue7TMQPMAe1h1OMJAXq856Y0AsVLXYKiBojUTfI11I7zVwYe08D8atIXHLZZw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-text@2.26.3': + resolution: {integrity: sha512-sGRbX96ss4jQeKw9d0iphuAWja8Dv4w4ryTDKfxD7Lizx3UaIxQB/y+Wna89tM3kfbi/qJcrD3AF7NJgfc/tEA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/pm@2.26.3': + resolution: {integrity: sha512-8gUmdxWlUevmgq2mNvGxvf2CpDW097tVKECMWKEn8sf846kXv3CoqaGRhI3db4kfR+09uWZeRM7rtrjRBmUThg==} + + '@tiptap/react@2.26.3': + resolution: {integrity: sha512-4g7pbdyawIO5YZXJQMwNv0dptblV4QUa7T/BYHe+PjAm4H+OeQbo7UmbxU427u8hPt1PhXZjbvT7D5i3r/MXCw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tryghost/adapter-base-cache@0.1.23': + resolution: {integrity: sha512-uOViNKfsG9X9+woQpUSCcDJhGsjU1O/SjpiXvUwCBBL+UzSp0b2J/kt0kOEhk5Rivr+LqTv/9Ijp876WSf0TFQ==} + + '@tryghost/admin-api-schema@4.7.2': + resolution: {integrity: sha512-k9s805e5vqZd5vwM3pooRVqTSb/Ku3+R8prATBNj+g+K8pEFisH2EerMZT49vURXmgJY752B+GuB/AOb66w3dw==} + + '@tryghost/api-framework@1.0.7': + resolution: {integrity: sha512-V+C/nVE4sgPQ3Vdm0jBAd05Za02inv68ZeO5rE2zYHUGp/PL5MfOPdhynTeFzzHNrmD9vzs+675C5zo/nLJY7w==} + + '@tryghost/bookshelf-collision@2.0.3': + resolution: {integrity: sha512-o7P9BzH4IE/KYOzDcfvq94hRf03eE3E3VqHjoi0AXYeT3C6f3dPr+Njq6gnXT/1zb1t/Kevoq9B2wCiHlBV2bg==} + + '@tryghost/bookshelf-custom-query@2.0.3': + resolution: {integrity: sha512-kAkeOxLxcGomjvriwzY81o3nr2szrOWSUhVTkCT98zTnn1y/qCxZWVp4AKZNNVBQ++dOHgeE1GIuml/EdxysTw==} + + '@tryghost/bookshelf-eager-load@2.0.3': + resolution: {integrity: sha512-NY/2hcDUY4i505lS6pyCKJePgygZYvuBE3Qna9uigKTagYU5G3VAkKQ4Q3cxXT5vS1OPvNEjmQB31MbNoVs0Dg==} + + '@tryghost/bookshelf-filter@2.0.3': + resolution: {integrity: sha512-GFDG8EIz1I2eIxsjFIoNAs9li9L0DDmHEaWnqJWtY6sVYQ3sVhuBkqXvfBFxoRHDzTQvuTC5ohcVhHjUt1MWMw==} + + '@tryghost/bookshelf-has-posts@2.1.0': + resolution: {integrity: sha512-iyUqzcms8cirB9mJ+CmOw80Evv8GhSdGOMhIELkmIHYvQbZ3uj9BPEcMan/CQJpjV/9CVVmjhlAtbP2btfzixg==} + + '@tryghost/bookshelf-include-count@2.0.3': + resolution: {integrity: sha512-mVkCTUu+Rkr7xKmpnnFyhSWIoo7ayETZ+B1k9aWag4uLUEQI8lR9bm7XSJZJPeX3O0pz9RzffohtQir08qr4Uw==} + + '@tryghost/bookshelf-order@2.0.3': + resolution: {integrity: sha512-LSu6oX8NdRPRwDia3Q63Xb8UViY13q6l5OBk1W5glJHA+jW9zfZhyRcB2Rz/YcWqx2iZ0mNG8W+X2+689G5qtA==} + + '@tryghost/bookshelf-pagination@2.0.3': + resolution: {integrity: sha512-cujW1io4sASVOm3HinpbcHXSafZd1POFmH66No4DA0d//eII4JGLyQDY2wC9t68ZlQTUolWWJ/za5trwA95iaQ==} + + '@tryghost/bookshelf-plugins@2.0.3': + resolution: {integrity: sha512-cPsVMst6a1iXZfMixbCmlQnp9WKg/ZCbR/YbSaK8BL7ADJZc3WgQHHjDM36GHpFVFb+SCksTaN2PGoM3u2zIHw==} + + '@tryghost/bookshelf-search@2.0.3': + resolution: {integrity: sha512-IgfueXsBeBPFriytip4V0t6q2sy4VqTOwIzGMtjVEweLJHCBkR7zJAnwLQcjaMMeg8aW56JuHFsyslqxDOaFSw==} + + '@tryghost/bookshelf-transaction-events@2.0.3': + resolution: {integrity: sha512-Mw2oJ75Qw7iJc7AfFpZeMpD9xpNm99snINOFNAC+EO2TW/swc7bn3GYqN2nkLtRuqayV3LwwICltXMVvUiShCw==} + + '@tryghost/bunyan-rotating-filestream@0.0.7': + resolution: {integrity: sha512-dswM+dxG8J7WpVoSjzAdoWXqqB5Dg0C2T7Zh6eoUvl5hkA8yWWJi/fS4jNXlHF700lWQ0g8/t+leJ7SGSWd+aw==} + + '@tryghost/color-utils@0.2.16': + resolution: {integrity: sha512-MFHLGIdT+mDimcvziHrvCivyZn5gbfNloLji9YwFAGOB3EGuh2WhGSdH47k9JEmZjQW5ihNr1WU1Fwi/m8zgXw==} + + '@tryghost/config-url-helpers@1.0.23': + resolution: {integrity: sha512-cknxQMmqhehtU9dMtlbyenXldWbhuxwUFl7Vzczq02yD6SO41UFvACq4ZhPGS5vb3mSPFCm8/OojHp5zxquAhg==} + + '@tryghost/config@2.0.3': + resolution: {integrity: sha512-hUC+OpeYKr8/5GJjc55KKe6M7r6S9MbnQZUEug572QY9f/iJwK1AwhbQ6rUR0iNOymPbv0MqUiT3lkYiB9nVbg==} + + '@tryghost/content-api@1.12.6': + resolution: {integrity: sha512-QCsSG10onLqpXKN7F8yHefOgRBtWdmi30s2dV7BYY5+/Iwgsaf/o/U5oTQhlsGkO3P73Zfw++tnqOL2wO9IYUA==} + + '@tryghost/custom-fonts@1.0.8': + resolution: {integrity: sha512-56epGhRXXk1QfKYxzyvnm7OCszdlWmMbw9XYq1w5L/67mPoQPX+4NkkNmZqlq/0CjNahzExOB0nOm7aHVDwvWQ==} + + '@tryghost/database-info@0.3.22': + resolution: {integrity: sha512-AwljcoXFUjSKFIoB/V9uohTLvWY8g+fIaHHjgPLH4Cq1tBZgcAlBIZR2ridhXiY1iDX7m3cws36uAxrN0KnoUw==} + + '@tryghost/database-info@0.3.35': + resolution: {integrity: sha512-S9OapApwzdh3GS0d3m+KgwH7IhZII6b9Aw5I89HA5VJRPeo6HdaDXiZzSDNcFaUzpx1FFBR0kDu7G+IRPD0eFA==} + + '@tryghost/debug@0.1.40': + resolution: {integrity: sha512-r8ecoTeoickPsn/59tkrouCNHhUEy79MNhJdPNkhx/Q3Oevw+SQBjnVYa5mHaxTaXryM4nNguKM5fbxPGPBo3Q==} + + '@tryghost/debug@2.0.3': + resolution: {integrity: sha512-qMlZc/1/OwbN8U/X4p5yFwqAbQZPcIZK9hwHRP/iTs+giYYQT7ZmWvsbxZYcvCIcA91Ia/PO7HZnOvinNF5ZHg==} + + '@tryghost/domain-events@1.0.8': + resolution: {integrity: sha512-4exHFUo4lRQO4f4l9FeNQEwCgfRtoUmnVBWr2WmDhVjZ09rQgW6ZrLe5f8CV7eMGQGllNjjz8780RZY9+UkONQ==} + + '@tryghost/elasticsearch@3.0.29': + resolution: {integrity: sha512-WGfoGbPXzX7SYPKeD7naYyoF8Lc7KruQPixFSJGLciRQ3wkrZQANQIguD4nA71upp0dhsT4D4aWP296VfJxuPw==} + + '@tryghost/email-mock-receiver@0.3.16': + resolution: {integrity: sha512-C72ZeaZa4eCZ4JSIY3BW4UhfBo34UA2O2+jEO7UMCsMT5AAIstv3oQgtj/kLZraFDaNtMOXdsUJA+gQDUTzBOA==} + + '@tryghost/ember-promise-modals@2.0.1': + resolution: {integrity: sha512-py/Fi3jbr+UiUped74m2VRhNEJQrFSgPEhQu/FSHkPjUPWruIokwJoO4TDedklGcMJ+0RKr3YiigcuMFxXjf7A==} + engines: {node: 12.* || >= 14.*} + + '@tryghost/errors@1.3.13': + resolution: {integrity: sha512-sXFcuU8Nn3mDcVrLBLFThQZImn+T2w7v/jJGJzdFnSKJstqxd1LyTRlMjpCpgCnqAgHFCaP+uFg7x4CX64VBJQ==} + + '@tryghost/express-test@0.15.5': + resolution: {integrity: sha512-a50AsvAjqMBP4UfEF+/2+ljPQ/oMA0rHSeoCk8Wwepp9P13Q+3Vl1mK5OjOfoBduhMYa/3kU+ABHeRrUqoiMxw==} + + '@tryghost/helpers@1.1.103': + resolution: {integrity: sha512-ako7nvUKySOaTMSZXUXnrg6pVTkxW1ZVKbJj0t0HGG+ndt0pjLBdnzdZW3BMGEj4Qvf0MWxGdUcLrW/OFyHzsg==} + + '@tryghost/html-to-mobiledoc@3.2.26': + resolution: {integrity: sha512-xNWtgY50yoEXqwd3+vswdz9BhpdlEbz1R63YnJeahslzQnhZkLSvf0bi+yrNuNTtlSqRJIictswwDShMdDFyLw==} + + '@tryghost/html-to-plaintext@1.0.8': + resolution: {integrity: sha512-CIXoOlmpp9SVrb0BPQ/+cBaSHFpGv53Gew3VFLw0w1v1q04+0e0FRKjl8Cv6oUl+c1I9LwxbwdcV6rxRCgVbIw==} + + '@tryghost/http-cache-utils@0.1.25': + resolution: {integrity: sha512-OJAD1ESvV+F81w6IOTKCX0JLGIiJqNn87HVFMskpUC3IUGpqcDP1pCM79d72khxLWy5oRQDYu8xIqicHJ9ST1Q==} + + '@tryghost/http-stream@0.1.42': + resolution: {integrity: sha512-wQBwTMLaMAFmKwi0nQeRPcnRvQYFtgYp9d4dJx0b1IkHLmhRwoRGcOkxYd6ajSEGfv7w0GkaaQH4Qd2s/pzuzw==} + + '@tryghost/image-transform@1.4.13': + resolution: {integrity: sha512-HkIXzNkNVPteAsucNcCpdLtsQr7332RufOgFyubb1hRkrj90swt5QWmAYCJX/HN1cb7mZr+xImqPwYNmsQ18jA==} + + '@tryghost/jest-snapshot@0.5.23': + resolution: {integrity: sha512-rhAkxKKlY2NB2+CGdXjY+aAsWA3xrZiR3yfTK+gnTRstuSR9bq8AU26oxE0L0zNCw0myTVF0WiyElCZ+e/4Gvw==} + + '@tryghost/job-manager@1.0.9': + resolution: {integrity: sha512-Wlwr1R8oeU6HLB7RB1g6VxAH6VEZIaXTVV2GdvCbipK+TA0MIo6YOQUgrIky25RpyRqKXR0UuDIfXDB4+S+GgQ==} + + '@tryghost/kg-card-factory@5.1.14': + resolution: {integrity: sha512-QwIUfF9xF2XMCIX4Sz78BvAQcAtC8D5l9iiFNjuszG/YeGsmBOpn2UN1VVjKqIJv5tKRHxWhm4Mp5PDra85ZHA==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-clean-basic-html@4.2.23': + resolution: {integrity: sha512-SwOUVciyumAtXADnTy4WKl+PnrG1MGrZeHSTADUzUkHKnetXqamIt5OVwgcygBe899l5B7EqSi81FaZ6PNBRMA==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-converters@1.1.21': + resolution: {integrity: sha512-RHXP6K6E/pR0rBQMk5v2stOdOpbLrbpapD3pD02jZs5zIsvbcnmurBTc1/zzC+OtxeRGoDirmm5PQXRvlRFU2Q==} + + '@tryghost/kg-default-atoms@5.1.9': + resolution: {integrity: sha512-woripWVlak8foKk6lKeyZHaH4O83LS9Esq8a2pW7LNY1mBk4Qovo4ydr/dZsfwVaLljfQkvRKvUypro2jSXNqA==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-default-cards@10.2.13': + resolution: {integrity: sha512-MQKDIh9i7zHoumjpmEJKZedW/MaIAKGCefZ02764fu6mbe6S5atwzDEB8YrAgvY+RPp/dmd8MM2zKuFhXKftGQ==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-default-nodes@2.0.21': + resolution: {integrity: sha512-FiFn30IDqp6FDH7b8/GIOZJAuzwJ929ZKNWZETk+qQzLVKJapl4KYV+63XGqnaYsW4l03fe7yj3xm17/Z0sHcQ==} + + '@tryghost/kg-default-transforms@1.2.44': + resolution: {integrity: sha512-+z53rNaHUNCKa02xDpU3JJA7m476hySIzaCTWY8F8/bpcOWGnSmdtiEYskjY6LGaDMcjqK3xQgI+eSuFl2o+CQ==} + + '@tryghost/kg-html-to-lexical@1.2.45': + resolution: {integrity: sha512-H5ety8dVMJSSDKMN0Rj8Zz30AmNbbVljuxLuZAZchSxPlScCpnbjjKWT6pIaVyjtVQrzzZSpQNt+AYT9RT0sYQ==} + + '@tryghost/kg-lexical-html-renderer@1.3.44': + resolution: {integrity: sha512-UMpau0tKJJqq+lyxtLByjyHktdg6dherIO6+hr4Y/jBxQ2ColIKIcfNemhm1wGWf5O1pUSYsqOsrUjH+CxSpnw==} + + '@tryghost/kg-markdown-html-renderer@7.1.18': + resolution: {integrity: sha512-VqK+3VnyX6acyJ28q4IuTpNi3rJJdCVFEgEwpQdDxDzbFxcj9vQVf7tl5gBPDGd8WeHcmsGU/ncWUTrahs+rpA==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-mobiledoc-html-renderer@7.1.18': + resolution: {integrity: sha512-ozlNEvq19oC2PQEescuMzhZPzWqwt2StSdS0nvTOXtoL+mgmuMSWazX0XAPZw0GPdfcpVlU7n9cDB098FkVBtw==} + engines: {node: ^22.13.1 || ^24.0.0} + + '@tryghost/kg-parser-plugins@4.2.25': + resolution: {integrity: sha512-NQk4E16eXFksNTUKRyXwyVid1oozfpe/Uc3lj5XXruS/qAc/N0avSR7woubHHH71bCAV9gIiEFlN0es1UC6/rQ==} + engines: {node: ^22.13.1} + + '@tryghost/kg-unsplash-selector@0.3.26': + resolution: {integrity: sha512-2hOwbJjxMQBXdebgkLXaJ6ez3pF8C6nbNZtOYFhs9wJKShxGgQtN8/dVU3CdHBC/DZTSdYFqLGv0WBCAHIi/mg==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + + '@tryghost/kg-utils@1.0.43': + resolution: {integrity: sha512-adCb45ViNgIqQHUBaG2E/Sp7bB/hmsc3t3sDsEKpruOtlhJIw7Qea1SF9VyHnnd81rRBB3rgFrQx2DqNaQraRA==} + + '@tryghost/koenig-lexical@1.7.30': + resolution: {integrity: sha512-4qjUMD79kj573kVwW5C1wMqmO1+lVm+fr7al6C6TMGncb9uiicCTTI+OKn/RpAYUeTtMAhBRqlqcgZ0nlvNykQ==} + + '@tryghost/limit-service@1.5.2': + resolution: {integrity: sha512-BTSnMpVMRauSkxi0yL/V2qVy9nRlAL9maALFAe5MDNTSCvg4zjV2b2S4o6+tgCyGDRkMPeePfrgSuH2YTTbD5Q==} + + '@tryghost/logging@2.5.5': + resolution: {integrity: sha512-OILuHgDuoO5Eq1aPrwAmxSPWPRF8AZI+EfyGusZVY70kdBH3XcxNySGZAZIvc0rAtXLOgQPSdQpxPfKbbBxPAg==} + + '@tryghost/members-csv@2.0.5': + resolution: {integrity: sha512-5BMzhWSoJg+pOQuXdO8ygHDxLFcwZLob903NX7lprWQtooLPD8FAWwdFql/ZA7gPG5Bq9lV7TyUFK3hWVtvIlw==} + + '@tryghost/metrics@1.0.43': + resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} + + '@tryghost/mobiledoc-kit@0.12.4-ghost.1': + resolution: {integrity: sha512-c4aheSWH2Y7x4uSkAx08gbtvuEgPGjlu6v+FeUdSJZ1blEd+knL3zTcUAfeSiM6rgLEHxlNWtt+KFwotdf6rTA==} + + '@tryghost/mongo-knex@0.9.4': + resolution: {integrity: sha512-R5mqpuQ1nN4qWAii9Tvg5POZ0nLO7Voy7QaQF+95sseUpfe4XEUsr3+mm4HYPC8MNKginlUm/unLf5QFzFQl5w==} + + '@tryghost/mongo-utils@0.6.3': + resolution: {integrity: sha512-BOgflEMywUXnTxXqOPuppFRD50IuIERAt06UEsyGTLRI6GSuO18BDygo72UpMHtM+NspGM0jPc1eQOeVvUJNAw==} + + '@tryghost/mw-error-handler@1.0.13': + resolution: {integrity: sha512-qDRjyiOkF5UNGgMHUDdFKTY0Mcwnxo5N9m71pliwVI+HfYvw77k/GxEIGc8mgikxjCuZkgC+IUnJjWCLuzCvKQ==} + + '@tryghost/mw-vhost@1.0.6': + resolution: {integrity: sha512-FKoSdBw5+fZNbzC58LD3ZLbMEj2UI5+4XOnEau7xINuYBP/c4wN0TXHuJT8y6H1wnYvO4Qu9L+PwHxe1yHBA+Q==} + + '@tryghost/nodemailer@0.3.48': + resolution: {integrity: sha512-Hl6S5yDfSsTkg5MITrdZj52e3rSkhYsoDQeyjtSD7iBwqCM4jq5TWBq+TEsohbArZJWxSpc09bAiKZusULIAtQ==} + + '@tryghost/nql-lang@0.6.4': + resolution: {integrity: sha512-m984kPFEexS3N1j/LeG51ChTJCplN/+IWoBP6/vsBxz0cnneY87J7rwr96gTmNR+s1xDzuQOpj3heoFD9Dw+7A==} + + '@tryghost/nql@0.12.10': + resolution: {integrity: sha512-kpj2ICTBmkz5Uet7Z/J61C/EEBTfa55np6LnbqW6N8g33uvCh9NkAsM2WgV1NK2lffpQT3Cs/qA2ymzHAguvoA==} + + '@tryghost/pretty-cli@1.2.52': + resolution: {integrity: sha512-+DGSSeKlNHQ/zqcs167W3W0D+HYz1TzAdK7TyzTBUj4Hodqxd70GfkdcWxLMNGdggKd4d59UxGZ/6n0vjVhVSA==} + + '@tryghost/pretty-cli@3.0.3': + resolution: {integrity: sha512-lAA6brlgQ5uyEDlpHdet7IZrspPYP5bRe6GVsnKQJ/RCTFoOC8fGhIy5E9UV37c6Ym4Jcjm/Z9adWXWiq4xhMg==} + + '@tryghost/pretty-stream@0.2.5': + resolution: {integrity: sha512-2bc5mk+7BAOt/mrcbjTsmUQhTKqysbIDOUUThSemPVbnij6487DhaRbnYWZW2IGKfAucrEO3h6TTm3kfioKJSw==} + + '@tryghost/prometheus-metrics@1.0.8': + resolution: {integrity: sha512-E1LRRwLgiWIN/P20uHgpTK+JVYYQ3+BarKCBszB6qmK5IBANJvQHI3LQV0wDWefVwFyl5FI6AqxLpOOpquvahA==} + + '@tryghost/promise@0.3.20': + resolution: {integrity: sha512-afHCBoS72XabqRi7GmHdVAWC/UMsVPGlxnL/epNVp0c/C7tEn6+MgHABXNAW4wViZFhuOda3k7fk3i2K0FAZQg==} + + '@tryghost/promise@0.3.8': + resolution: {integrity: sha512-ppcnLBWczpbo4sQcGWtjEA82kdZMv4NFF2MvZRi1MBP4lSOSgh9A636eUxlB1/FpIG+D5ixq84xlY4QJMqW2kA==} + + '@tryghost/referrer-parser@0.1.15': + resolution: {integrity: sha512-d/C1iim7X08dDBB6+xt45cUAtjyF4sje8Z+tjJ00heC+kcuZca1m/5YqosWytUCPvUpq0G5jeatZYTK8r19lag==} + engines: {node: '>=16.0.0'} + + '@tryghost/request@1.0.12': + resolution: {integrity: sha512-vSsyaQ06ySTkDF78JALsLMfOGTFWfRCye3s1zys3sUZy2A51YsmP7kcg5wCemgTTDnRhErOUBotUuECIyg0TGA==} + + '@tryghost/request@1.0.17': + resolution: {integrity: sha512-YbxolgTuIORAUUSjfQWJfG1U2sSlZMhXjl+M7/NLvIgCc+Uzx8XjpIrYW6NDAn+lTt9/WAx/36EWr3nzVnuTHg==} + + '@tryghost/root-utils@0.3.38': + resolution: {integrity: sha512-ARn8wC6qv867lCr7BZ+IS8S/88K5go0j6HgQFhP27rKje4b40PsxH/P3rO4Ez2NzF/Do8ywFrTWHoLCSsCazXQ==} + + '@tryghost/root-utils@2.0.3': + resolution: {integrity: sha512-7fF7QLwiOoVzHVd5Qci8gsrqFIrZP5FwVrwGjSxjjF5hoOtgPn7HLdHLyIHUY9T5KgJ1/69i573E/EA+4kmAfA==} + + '@tryghost/security@1.0.6': + resolution: {integrity: sha512-h4FiUK4ndHezlXeuRzfwTZrX/a8ZdCS/FRqmZWCHcMUiBH6lewHv8eIjsPemN6e6Gn9jtsWZsKnuYM2mD0meSQ==} + + '@tryghost/server@2.0.3': + resolution: {integrity: sha512-JP2f0YJ1pKgQWYwjPCvaOZN80RSDdWk1kfpi1s/3LH0HtSyh6yeDoRPrcX51SpHm/4JNQ60WzD56aGtC5bGaHQ==} + + '@tryghost/social-urls@0.1.60': + resolution: {integrity: sha512-lbhdaIwV6XrhyML+ugarYKn2BDam3c/LsLYyezTFZGtFUWmJgm/DvOY2Trd2YATi7iLifLsWhXGhIr5v+hKL3w==} + + '@tryghost/string@0.2.21': + resolution: {integrity: sha512-ILG3h+I432rlgD46tHrugkxEnJ8mh73JxKco4B/uIlBDv5evZjq9psIXt595ZA0Q8DuKMQKZUeVZYV9GBLA1eQ==} + + '@tryghost/string@0.3.2': + resolution: {integrity: sha512-tPgTU4o/4m1TBMoSChHm4n7XvXq+x6GJ//B0MdVPK57Rb0GEThsV347C2Fr5+1q71EdICrLbFPYh0vAqEM/eoA==} + + '@tryghost/timezone-data@0.4.18': + resolution: {integrity: sha512-yawVt4pBmbsw2Uwsxd/8rjwyucQX6cefHH48X5Qe5eMQH3mnla5Q+gJ+By9a3Sza1qECvhIG3jUhDM3cLhILzQ==} + + '@tryghost/tpl@0.1.40': + resolution: {integrity: sha512-8w94lbNxXptRCIS8pe/IHjzgVFLxljxXx8+UQLO8NRFKraoEXthkPjAphN2MXKp1UGtj5ldh4Ye4D82bUWceOw==} + + '@tryghost/tpl@2.0.3': + resolution: {integrity: sha512-v3BCkpcjEpYG7gPBntOjUYhADYfxbb+3tlt0KxqhZ9RYm+1sQ6wdsfZ+6Yh0akOwp/caifWIVuuvfcbL7x4SFA==} + + '@tryghost/url-utils@5.1.2': + resolution: {integrity: sha512-GGZRfdVdk7jE1puqfBCeaAMeQ/Gd0NUkbqhj4gRSCWKW97JDpoiu5Fwyutt3vq3U3X4ZsfLfKs0NtAfUez0oog==} + + '@tryghost/url-utils@5.2.2': + resolution: {integrity: sha512-KlDLms43V9j3LAft3bhHYuywhaAob9fO1htbSOH++hGEP9nQLL9FHLYqeuWzXvKTb+ZjGZ+HNRpeErc7G6dgCw==} + + '@tryghost/validator@0.2.22': + resolution: {integrity: sha512-dmobNVEKXMi3K4OdAXLBFwa78hVgy8cYvCPJgfV4h3NtYIyRHqwARr3upT3/ASpVBpKGVBu7XexfSv+tibzsuQ==} + + '@tryghost/version@0.1.38': + resolution: {integrity: sha512-kVaM7eijFRrBLZLDbmiFxHQ/shmAg5UQ9yM4s9GZN4M3OWY34UHNmjoSOniOHnA5E8fYNA8A3IFmS3FW910xYg==} + + '@tryghost/webhook-mock-receiver@0.2.22': + resolution: {integrity: sha512-QthCC6nmXlDKzuhZkzdZOatpgbJlGcJS6Et7BMfHJ6z/56QA21iCC1nRyLa5oCbh21kXtWZpGvX/heRgu2g07Q==} + + '@tryghost/zip@1.1.54': + resolution: {integrity: sha512-6JpuYYlNytFkWTgpZnbkkoH5rqd9FIHfY0ekNNHWZFBS32D6o6bwV4v2wqFmsd0GzMG/CxaHfxfGU+HfTVTJjw==} + + '@tryghost/zip@3.0.3': + resolution: {integrity: sha512-gp8z5JYH5h1MGeS+MQSTsrf33XxSTOgFHJCLstcxIV5hg2pxSzo7N4Gg3+CrqrqIqLNO0aEkOkikAFtArpPwdQ==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/bluebird@3.5.42': + resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bookshelf@1.2.9': + resolution: {integrity: sha512-WP1mPCN7pd0DaPYhsy9tgkR5HhZz9Zu1s2/DipNWwC5IfAEQjiEtH+l4PrgvtcTNS1xbUPn4+vfytjJvsZ2+CQ==} + + '@types/broccoli-plugin@1.3.0': + resolution: {integrity: sha512-SLk4/hFc2kGvgwNFrpn2O1juxFOllcHAywvlo7VwxfExLzoz1GGJ0oIZCwj5fwSpvHw4AWpZjJ1fUvb62PDayQ==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai-as-promised@7.1.8': + resolution: {integrity: sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==} + + '@types/chai@4.3.20': + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/color-convert@2.0.4': + resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==} + + '@types/color-convert@3.0.1': + resolution: {integrity: sha512-Wj7O4Y7tPo/3y9z4K0XRLbLBP2hCHq2vlmLkR0uQDGmLdoUVDmUrXy50ZffMxZKzBpYxFT+4FnDT8xzMlnKRQQ==} + deprecated: This is a stub types definition. color-convert provides its own type definitions, so you do not need this installed. + + '@types/color-name@1.1.5': + resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==} + + '@types/color@4.2.0': + resolution: {integrity: sha512-6+xrIRImMtGAL2X3qYkd02Mgs+gFGs+WsK0b7VVMaO4mYRISwyTjcqNrO0mNSmYEoq++rSLDB2F5HDNmqfOe+A==} + + '@types/color@4.2.1': + resolution: {integrity: sha512-ResWeDLy1vozIMbD6JLRKuNBbIcIlBkjTIxVHHd5Cqtm77T+ahH3BWE/PWv1OhFd1HAwcn8no4ig2uTaRXpYQQ==} + + '@types/common-tags@1.8.4': + resolution: {integrity: sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/create-error@0.3.33': + resolution: {integrity: sha512-KJW8os1BxoiY9MtyAS0NqSiH4eGDpCC0N/7tQZy3XN3l4YbLnpfFf91jnfW3HXFoyWyPQBTZ4FJICMIFBJ0tKA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.47': + resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@8.56.12': + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/find-cache-dir@3.2.1': + resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} + + '@types/fs-extra@5.1.0': + resolution: {integrity: sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==} + + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/glob@9.0.0': + resolution: {integrity: sha512-00UxlRaIUvYm4R4W9WYkN8/J+kV8fmOQ7okeH6YFtGWFMt3odD45tpG5yA5wnL7HE6lLgjaTW5n14ju2hl2NNA==} + deprecated: This is a stub types definition. glob provides its own type definitions, so you do not need this installed. + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/jsdom@28.0.1': + resolution: {integrity: sha512-GJq2QE4TAZ5ajSoCasn5DOFm8u1mI3tIFvM5tIq3W5U/RTB6gsHwc6Yhpl91X9VSDOUVblgXmG+2+sSvFQrdlw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/mime-types@3.0.1': + resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node-jose@1.1.13': + resolution: {integrity: sha512-QjMd4yhwy1EvSToQn0YI3cD29YhyfxFwj7NecuymjLys2/P0FwxWnkgBlFxCai6Y3aBCe7rbwmqwJJawxlgcXw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/node@9.6.61': + resolution: {integrity: sha512-/aKAdg5c8n468cYLy2eQrcR5k6chlbNwZNGUj3TboyPa2hcO2QAJcfymlqPzMiRj8B6nYKXjzQz36minFE0RwQ==} + + '@types/nodemailer@6.4.23': + resolution: {integrity: sha512-aFV3/NsYFLSx9mbb5gtirBSXJnAlrusoKNuPbxsASWc7vrKLmIrTQRpdcxNcSFL3VW2A2XpeLEavwb2qMi6nlQ==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/on-headers@1.0.4': + resolution: {integrity: sha512-D8Dy7oiM4fFQPA1TtHVTGPFNgCCYY132dTFLezhvlPiiiyKkuW6tVipPXpUOiuTUu8Kzd+ASKjULvZld197Dhw==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + + '@types/prettier@2.7.3': + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + + '@types/pretty-hrtime@1.0.3': + resolution: {integrity: sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/q@1.5.8': + resolution: {integrity: sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@17.0.26': + resolution: {integrity: sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==} + peerDependencies: + '@types/react': ^17.0.0 + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-svg-map@2.1.4': + resolution: {integrity: sha512-M6F/be3ecPgtOz8AubD8nYY23Q+OSJ6t1L/D6hr6fRyfykTiSv20r9sK4xnQSbWiBDFoR3rTDySp3oD1JGpjFA==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react-world-flags@1.6.0': + resolution: {integrity: sha512-j/uVy2fnG8gX3Ckic4sccYm9XjieasUsJDMqBDtdPdcwe3aFfz+iBbds+wxOiTzfe5BErVGjdFu6NO1hCg/7lw==} + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/rimraf@2.0.5': + resolution: {integrity: sha512-YyP+VfeaqAyFmXoTh3HChxOQMyjByRMsHU7kc5KOJkSlXudhMhQIALbYV7rHh/l8d2lX3VUQzprrcAgWdRuU8g==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/sinon@17.0.4': + resolution: {integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==} + + '@types/sinonjs__fake-timers@15.0.1': + resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/symlink-or-copy@1.2.2': + resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/testing-library__jest-dom@5.14.9': + resolution: {integrity: sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.49.0': + resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.49.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.49.0': + resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.49.0': + resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.49.0': + resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.57.2': + resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.49.0': + resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.49.0': + resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.49.0': + resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.49.0': + resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.49.0': + resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@uiw/codemirror-extensions-basic-setup@4.25.2': + resolution: {integrity: sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.2': + resolution: {integrity: sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react-swc@4.1.0': + resolution: {integrity: sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/coverage-v8@0.34.6': + resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} + peerDependencies: + vitest: '>=0.32.0 <1' + + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + peerDependencies: + vitest: 3.2.4 + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/ast@1.9.0': + resolution: {integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/floating-point-hex-parser@1.9.0': + resolution: {integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-api-error@1.9.0': + resolution: {integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-buffer@1.9.0': + resolution: {integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==} + + '@webassemblyjs/helper-code-frame@1.9.0': + resolution: {integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==} + + '@webassemblyjs/helper-fsm@1.9.0': + resolution: {integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==} + + '@webassemblyjs/helper-module-context@1.9.0': + resolution: {integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-bytecode@1.9.0': + resolution: {integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/helper-wasm-section@1.9.0': + resolution: {integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/ieee754@1.9.0': + resolution: {integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/leb128@1.9.0': + resolution: {integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/utf8@1.9.0': + resolution: {integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-edit@1.9.0': + resolution: {integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-gen@1.9.0': + resolution: {integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-opt@1.9.0': + resolution: {integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wasm-parser@1.9.0': + resolution: {integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==} + + '@webassemblyjs/wast-parser@1.9.0': + resolution: {integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@webassemblyjs/wast-printer@1.9.0': + resolution: {integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==} + + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@yarnpkg/parsers@3.0.2': + resolution: {integrity: sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==} + engines: {node: '>=18.12.0'} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abortcontroller-polyfill@1.7.8: + resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} + + abstract-leveldown@0.12.4: + resolution: {integrity: sha512-TOod9d5RDExo6STLMGa+04HGkl+TlMfbDnTyN93/ETJ9DpQ0DaYLqcMZlbXvdc4W3vVo1Qrl+WhSp8zvDsJ+jA==} + deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-dynamic-import@3.0.0: + resolution: {integrity: sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==} + deprecated: This is probably built in to whatever tool you're using. If you still need it... idk + + acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@5.7.4: + resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@6.4.2: + resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + adjust-sourcemap-loader@4.0.0: + resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} + engines: {node: '>=8.9'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-errors@1.0.1: + resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} + peerDependencies: + ajv: '>=5.0.0' + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + alphanum-sort@1.0.2: + resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==} + + amd-name-resolver@1.2.0: + resolution: {integrity: sha512-hlSTWGS1t6/xq5YCed7YALg7tKZL3rkl7UwEZ/eCIkn8JxmM6fU6Qs/1hwtjQqfuYxlffuUcgYEm0f5xP4YKaA==} + + amd-name-resolver@1.3.1: + resolution: {integrity: sha512-26qTEWqZQ+cxSYygZ4Cf8tsjDBLceJahhtewxtKZA3SRa4PluuqYCuheemDQD+7Mf5B7sr+zhTDWAHDh02a1Dw==} + engines: {node: 6.* || 8.* || >= 10.*} + + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + + animejs@3.2.2: + resolution: {integrity: sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-html@0.0.7: + resolution: {integrity: sha512-JoAxEa1DfP9m2xfB/y2r/aKcwXNlltr4+0QSBC4TrLfcxyvepX2Pv0t/xpgGV5bGsDzCYV8SzjWgyCW0T9yYbA==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansi-to-html@0.6.15: + resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==} + engines: {node: '>=8.0.0'} + hasBin: true + + ansicolors@0.2.1: + resolution: {integrity: sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-root-dir@1.0.2: + resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} + + app-root-path@2.2.1: + resolution: {integrity: sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==} + engines: {node: '>= 6.0.0'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + applause@2.0.4: + resolution: {integrity: sha512-wFhNjSoflbAEgelX3psyKSXV2iQFjuYW31DEhcCOD/bQ98VdfltLclK4p1mI6E58Qp4Q7+5RCbBdr+Nc9b5QhA==} + engines: {node: '>=10'} + + aproba@1.2.0: + resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + are-we-there-yet@1.1.7: + resolution: {integrity: sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==} + deprecated: This package is no longer supported. + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-each@1.0.1: + resolution: {integrity: sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==} + engines: {node: '>=0.10.0'} + + array-equal@1.0.2: + resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-slice@1.1.0: + resolution: {integrity: sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==} + engines: {node: '>=0.10.0'} + + array-to-error@1.1.1: + resolution: {integrity: sha512-kqcQ8s7uQfg3UViYON3kCMcck3A9exxgq+riVuKy08Mx00VN4EJhK30L2VpjE58LQHKhcE/GRpvbVUhqTvqzGQ==} + + array-to-sentence@1.1.0: + resolution: {integrity: sha512-YkwkMmPA2+GSGvXj1s9NZ6cc2LBtR+uSeWTy2IGi5MR1Wag4DdrcjTxA/YV/Fw+qKlBeXomneZgThEbm/wvZbw==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.reduce@1.0.8: + resolution: {integrity: sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1.js@4.10.1: + resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assert@1.5.1: + resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + ast-types@0.13.3: + resolution: {integrity: sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==} + engines: {node: '>=4'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-disk-cache@1.3.5: + resolution: {integrity: sha512-VZpqfR0R7CEOJZ/0FOTgWq70lCrZyS1rkI8PXugDUkTKyyAUgZ2zQ09gLhMkEn+wN8LYeUTPxZdXtlX/kmbXKQ==} + + async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-promise-queue@1.0.5: + resolution: {integrity: sha512-xi0aQ1rrjPWYmqbwr18rrSKbSaXIeIwSd1J4KAgVfkq8utNbdZoht7GfvfY6swFUAMJ9obkc4WPJmtGwl+B8dw==} + + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.3: + resolution: {integrity: sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + + audio-extensions@0.0.0: + resolution: {integrity: sha512-yj9C819u3ED3/OyRd9mLKMXGy8wsElaf6bkkv6OqZEKrNAT461TjiznS4IfPBy8Mmh6DWaXCQCVYSq3+VHkpjQ==} + engines: {node: '>=0.10.0'} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + autoprefixer@9.8.6: + resolution: {integrity: sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==} + hasBin: true + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + + b4a@1.8.0: + resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + babel-code-frame@6.26.0: + resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} + + babel-core@6.26.3: + resolution: {integrity: sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==} + + babel-generator@6.26.1: + resolution: {integrity: sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==} + + babel-helper-builder-binary-assignment-operator-visitor@6.24.1: + resolution: {integrity: sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==} + + babel-helper-builder-react-jsx@6.26.0: + resolution: {integrity: sha512-02I9jDjnVEuGy2BR3LRm9nPRb/+Ja0pvZVLr1eI5TYAA/dB0Xoc+WBo50+aDfhGDLhlBY1+QURjn9uvcFd8gzg==} + + babel-helper-call-delegate@6.24.1: + resolution: {integrity: sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==} + + babel-helper-define-map@6.26.0: + resolution: {integrity: sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==} + + babel-helper-explode-assignable-expression@6.24.1: + resolution: {integrity: sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==} + + babel-helper-function-name@6.24.1: + resolution: {integrity: sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==} + + babel-helper-get-function-arity@6.24.1: + resolution: {integrity: sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==} + + babel-helper-hoist-variables@6.24.1: + resolution: {integrity: sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==} + + babel-helper-optimise-call-expression@6.24.1: + resolution: {integrity: sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==} + + babel-helper-regex@6.26.0: + resolution: {integrity: sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==} + + babel-helper-remap-async-to-generator@6.24.1: + resolution: {integrity: sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==} + + babel-helper-replace-supers@6.24.1: + resolution: {integrity: sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==} + + babel-helpers@6.24.1: + resolution: {integrity: sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==} + + babel-import-util@0.2.0: + resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} + engines: {node: '>= 12.*'} + + babel-import-util@1.4.1: + resolution: {integrity: sha512-TNdiTQdPhXlx02pzG//UyVPSKE7SNWjY0n4So/ZnjQpWwaM5LvWBLkWa1JKll5u06HNscHD91XZPuwrMg1kadQ==} + engines: {node: '>= 12.*'} + + babel-import-util@2.1.1: + resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} + engines: {node: '>= 12.*'} + + babel-import-util@3.0.1: + resolution: {integrity: sha512-2copPaWQFUrzooJVIVZA/Oppx/S/KOoZ4Uhr+XWEQDMZ8Rvq/0SNQpbdIyMBJ8IELWt10dewuJw+tX4XjOo7Rg==} + engines: {node: '>= 12.*'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-loader@8.4.1: + resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + + babel-messages@6.23.0: + resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} + + babel-plugin-check-es2015-constants@6.22.0: + resolution: {integrity: sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==} + + babel-plugin-debug-macros@0.2.0: + resolution: {integrity: sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-beta.42 + + babel-plugin-debug-macros@0.3.4: + resolution: {integrity: sha512-wfel/vb3pXfwIDZUrkoDrn5FHmlWI96PCJ3UCDv2a86poJ3EQrnArNW5KfHSVJ9IOgxHbo748cQt7sDU+0KCEw==} + engines: {node: '>=6'} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-plugin-ember-data-packages-polyfill@0.1.2: + resolution: {integrity: sha512-kTHnOwoOXfPXi00Z8yAgyD64+jdSXk3pknnS7NlqnCKAU6YDkXZ4Y7irl66kaZjZn0FBBt0P4YOZFZk85jYOww==} + engines: {node: 6.* || 8.* || 10.* || >= 12.*} + + babel-plugin-ember-modules-api-polyfill@2.13.4: + resolution: {integrity: sha512-uxQPkEQAzCYdwhZk16O9m1R4xtCRNy4oEUTBrccOPfzlIahRZJic/JeP/ZEL0BC6Mfq6r55eOg6gMF/zdFoCvA==} + engines: {node: 6.* || 8.* || >= 10.*} + + babel-plugin-ember-modules-api-polyfill@3.5.0: + resolution: {integrity: sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA==} + engines: {node: 6.* || 8.* || >= 10.*} + + babel-plugin-ember-template-compilation@2.4.1: + resolution: {integrity: sha512-n+ktQ3JeyWrpRutSyPn2PsHeH+A94SVm+iUoogzf9VUqpP47FfWem24gpQXhn+p6+x5/BpuFJXMLXWt7ZoYAKA==} + engines: {node: '>= 12.*'} + + babel-plugin-filter-imports@4.0.0: + resolution: {integrity: sha512-jDLlxI8QnfKd7PtieH6pl4tZJzymzfCDCPGdTq/grgbiYAikwDPp/oL0IlFJn0HQjLpcLkyYhPKkUVneRESw5w==} + engines: {node: '>=8'} + + babel-plugin-htmlbars-inline-precompile@1.0.0: + resolution: {integrity: sha512-4jvKEHR1bAX03hBDZ94IXsYCj3bwk9vYsn6ux6JZNL2U5pvzCWjqyrGahfsGNrhERyxw8IqcirOi9Q6WCo3dkQ==} + engines: {node: '>= 4'} + + babel-plugin-htmlbars-inline-precompile@3.2.0: + resolution: {integrity: sha512-IUeZmgs9tMUGXYu1vfke5I18yYJFldFGdNFQOWslXTnDWXzpwPih7QFduUqvT+awDpDuNtXpdt5JAf43Q1Hhzg==} + engines: {node: 8.* || 10.* || >= 12.*} + + babel-plugin-htmlbars-inline-precompile@5.3.1: + resolution: {integrity: sha512-QWjjFgSKtSRIcsBhJmEwS2laIdrA6na8HAlc/pEAhjHgQsah/gMiBFRZvbQTy//hWxR4BMwV7/Mya7q5H8uHeA==} + engines: {node: 10.* || >= 12.*} + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-module-resolver@3.2.0: + resolution: {integrity: sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA==} + engines: {node: '>= 6.0.0'} + + babel-plugin-module-resolver@4.1.0: + resolution: {integrity: sha512-MlX10UDheRr3lb3P0WcaIdtCSRlxdQsB1sBqL7W0raF070bGl1HQQq5K3T2vf2XAYie+ww+5AKC/WrkjRO2knA==} + engines: {node: '>= 8.0.0'} + + babel-plugin-module-resolver@5.0.3: + resolution: {integrity: sha512-h8h6H71ZvdLJZxZrYkaeR30BojTaV7O9GfqacY14SNj5CNB8ocL9tydNzTC0JrnNN7vY3eJhwCmkDj7tuEUaqQ==} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-syntax-async-functions@6.13.0: + resolution: {integrity: sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==} + + babel-plugin-syntax-class-properties@6.13.0: + resolution: {integrity: sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==} + + babel-plugin-syntax-dynamic-import@6.18.0: + resolution: {integrity: sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==} + + babel-plugin-syntax-exponentiation-operator@6.13.0: + resolution: {integrity: sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==} + + babel-plugin-syntax-jsx@6.18.0: + resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} + + babel-plugin-syntax-trailing-function-commas@6.22.0: + resolution: {integrity: sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==} + + babel-plugin-transform-async-to-generator@6.24.1: + resolution: {integrity: sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==} + + babel-plugin-transform-class-properties@6.24.1: + resolution: {integrity: sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==} + + babel-plugin-transform-es2015-arrow-functions@6.22.0: + resolution: {integrity: sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==} + + babel-plugin-transform-es2015-block-scoped-functions@6.22.0: + resolution: {integrity: sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==} + + babel-plugin-transform-es2015-block-scoping@6.26.0: + resolution: {integrity: sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==} + + babel-plugin-transform-es2015-classes@6.24.1: + resolution: {integrity: sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==} + + babel-plugin-transform-es2015-computed-properties@6.24.1: + resolution: {integrity: sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==} + + babel-plugin-transform-es2015-destructuring@6.23.0: + resolution: {integrity: sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==} + + babel-plugin-transform-es2015-duplicate-keys@6.24.1: + resolution: {integrity: sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==} + + babel-plugin-transform-es2015-for-of@6.23.0: + resolution: {integrity: sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==} + + babel-plugin-transform-es2015-function-name@6.24.1: + resolution: {integrity: sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==} + + babel-plugin-transform-es2015-literals@6.22.0: + resolution: {integrity: sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==} + + babel-plugin-transform-es2015-modules-amd@6.24.1: + resolution: {integrity: sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==} + + babel-plugin-transform-es2015-modules-commonjs@6.26.2: + resolution: {integrity: sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==} + + babel-plugin-transform-es2015-modules-systemjs@6.24.1: + resolution: {integrity: sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==} + + babel-plugin-transform-es2015-modules-umd@6.24.1: + resolution: {integrity: sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==} + + babel-plugin-transform-es2015-object-super@6.24.1: + resolution: {integrity: sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==} + + babel-plugin-transform-es2015-parameters@6.24.1: + resolution: {integrity: sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==} + + babel-plugin-transform-es2015-shorthand-properties@6.24.1: + resolution: {integrity: sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==} + + babel-plugin-transform-es2015-spread@6.22.0: + resolution: {integrity: sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==} + + babel-plugin-transform-es2015-sticky-regex@6.24.1: + resolution: {integrity: sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==} + + babel-plugin-transform-es2015-template-literals@6.22.0: + resolution: {integrity: sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==} + + babel-plugin-transform-es2015-typeof-symbol@6.23.0: + resolution: {integrity: sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==} + + babel-plugin-transform-es2015-unicode-regex@6.24.1: + resolution: {integrity: sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==} + + babel-plugin-transform-exponentiation-operator@6.24.1: + resolution: {integrity: sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==} + + babel-plugin-transform-react-jsx@6.24.1: + resolution: {integrity: sha512-s+q/Y2u2OgDPHRuod3t6zyLoV8pUHc64i/O7ZNgIOEdYTq+ChPeybcKBi/xk9VI60VriILzFPW+dUxAEbTxh2w==} + + babel-plugin-transform-regenerator@6.26.0: + resolution: {integrity: sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==} + + babel-plugin-transform-strict-mode@6.24.1: + resolution: {integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==} + + babel-polyfill@6.26.0: + resolution: {integrity: sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-env@1.7.0: + resolution: {integrity: sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==} + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-register@6.26.0: + resolution: {integrity: sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==} + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + + babel-template@6.26.0: + resolution: {integrity: sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==} + + babel-traverse@6.26.0: + resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} + + babel-types@6.26.0: + resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} + + babel6-plugin-strip-class-callcheck@6.0.0: + resolution: {integrity: sha512-biNFJ7JAK4+9BwswDGL0dmYpvXHvswOFR/iKg3Q/f+pNxPEa5bWZkLHI1fW4spPytkHGMe7f/XtYyhzml9hiWg==} + + babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + + backbone@1.6.1: + resolution: {integrity: sha512-YQzWxOrIgL6BoFnZjThVN99smKYhyEXXFyJJ2lsF1wJLyo4t+QjmkLrH8/fN22FZ4ykF70Xq7PgTugJVR4zS9Q==} + + bail@1.0.5: + resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.5.6: + resolution: {integrity: sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.8.0: + resolution: {integrity: sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.11.0: + resolution: {integrity: sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.0: + resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + base64-arraybuffer@0.2.0: + resolution: {integrity: sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + baseline-browser-mapping@2.10.11: + resolution: {integrity: sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==} + engines: {node: '>=6.0.0'} + hasBin: true + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + batch-processor@1.0.0: + resolution: {integrity: sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + binaryextensions@2.3.0: + resolution: {integrity: sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==} + engines: {node: '>=0.8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + + bl@0.8.2: + resolution: {integrity: sha512-pfqikmByp+lifZCS0p6j6KreV6kNU6Apzpm2nKOk+94cZb/jvle55+JxWiByUQ0Wo/+XnDXEy5MxxKMb6r0VIw==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bl@5.1.0: + resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} + + blank-object@1.0.2: + resolution: {integrity: sha512-kXQ19Xhoghiyw66CUiGypnuRpWlbHAzY/+NyvqTEdTfhfQGH1/dbEMYiXju7fYKIFePpzp/y9dsu5Cu/PkmawQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + + bn.js@5.2.3: + resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + body@5.1.0: + resolution: {integrity: sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==} + + bookshelf-relations@2.8.0: + resolution: {integrity: sha512-cOywFvCSD5j45X5n8gJ7VHR1e36HXVnG88ppF03imCCkWkaWW0WJUD28FSdIzYs3DBYLv3eyGjqeYRnk0a4JHg==} + engines: {node: ^18.12.1 || ^20.11.1 || ^22.13.1} + peerDependencies: + bookshelf: '>=1.1.0' + + bookshelf@1.2.0: + resolution: {integrity: sha512-rm04YpHkLej6bkNezKUQjzuXV30rbyEHQoaKvfQ3fOyLYxPeB18uBL+h2t6SmeXjfsB+aReMmbhkMF/lUTbtMA==} + engines: {node: '>=6'} + peerDependencies: + knex: '>=0.15.0 <0.22.0' + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + bower-config@1.4.3: + resolution: {integrity: sha512-MVyyUk3d1S7d2cl6YISViwJBc2VXCkxF5AUFykvN0PQj5FsUiMNSgAYTso18oRFfyZ6XEtjrgg9MAaufHbOwNw==} + engines: {node: '>=0.8.0'} + + bower-endpoint-parser@0.2.2: + resolution: {integrity: sha512-YWZHhWkPdXtIfH3VRu3QIV95sa75O9vrQWBOHjexWCLBCTy5qJvRr36LXTqFwTchSXVlzy5piYJOjzHr7qhsNg==} + engines: {node: '>=0.8.0'} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bree@6.5.0: + resolution: {integrity: sha512-Yzoflt/zwaRQeF1Gurjbn0g49kZ9QTA7rWR0IKQliKiUJSrsuGbtyBToI5WV60dmd+SEVPtu0oJCjYbEeduYyw==} + engines: {node: '>= 10'} + deprecated: bree@7.0.0 drops support for the browser, Node <12.11.0, and removes bthreads entirely. Either upgrade to v7.0.0 or lock your bree version to v6.5.0. + + broccoli-amd-funnel@2.0.1: + resolution: {integrity: sha512-VRE+0PYAN4jQfkIq3GKRj4U/4UV9rVpLan5ll6fVYV4ziVg4OEfR5GUnILEg++QtR4xSaugRxCPU5XJLDy3bNQ==} + engines: {node: '>=6'} + + broccoli-asset-rev@3.0.0: + resolution: {integrity: sha512-gAHQZnwvtl74tGevUqGuWoyOdJUdMMv0TjGSMzbdyGImr9fZcnM6xmggDA8bUawrMto9NFi00ZtNUgA4dQiUBw==} + + broccoli-asset-rewrite@2.0.0: + resolution: {integrity: sha512-dqhxdQpooNi7LHe8J9Jdxp6o3YPFWl4vQmint6zrsn2sVbOo+wpyiX3erUSt0IBtjNkAxqJjuvS375o2cLBHTA==} + + broccoli-babel-transpiler@6.5.1: + resolution: {integrity: sha512-w6GcnkxvHcNCte5FcLGEG1hUdQvlfvSN/6PtGWU/otg69Ugk8rUk51h41R0Ugoc+TNxyeFG1opRt2RlA87XzNw==} + engines: {node: '>= 4'} + + broccoli-babel-transpiler@7.8.1: + resolution: {integrity: sha512-6IXBgfRt7HZ61g67ssBc6lBb3Smw3DPZ9dEYirgtvXWpRZ2A9M22nxy6opEwJDgDJzlu/bB7ToppW33OFkA1gA==} + engines: {node: '>= 6'} + + broccoli-babel-transpiler@8.0.2: + resolution: {integrity: sha512-XIGsUyJgehSRNVVrOnRuW+tijYBqkoGEONc/UHkiOBW+C0trPv9c/Icc/Cf+2l1McQlHW/Mc6SksHg6qFlEClg==} + engines: {node: 16.* || >= 18} + peerDependencies: + '@babel/core': ^7.17.9 + + broccoli-builder@0.18.14: + resolution: {integrity: sha512-YoUHeKnPi4xIGZ2XDVN9oHNA9k3xF5f5vlA+1wvrxIIDXqQU97gp2FxVAF503Zxdtt0C5CRB5n+47k2hlkaBzA==} + engines: {node: '>= 0.10.0'} + + broccoli-caching-writer@3.1.0: + resolution: {integrity: sha512-3TWi92ogzUhLmCF5V4DjhN7v4t6OjXYO21p9GkuOZQ1SiVmM1sYio364y64dREHUzjFEcH8mdVCiRDdrwUGVTw==} + + broccoli-clean-css@1.1.0: + resolution: {integrity: sha512-S7/RWWX+lL42aGc5+fXVLnwDdMtS0QEWUFalDp03gJ9Na7zj1rWa351N2HZ687E2crM9g+eDWXKzD17cbcTepg==} + + broccoli-concat@4.2.7: + resolution: {integrity: sha512-JePfBFwHtZ2FR33PBZQA99/hQ4idIbZ205rH84Jw6vgkuKDRVXWVzZP2gvR2WXugXaQ1fj3+yO04b0QsstNHzQ==} + engines: {node: 10.* || >= 12.*} + + broccoli-config-loader@1.0.1: + resolution: {integrity: sha512-MDKYQ50rxhn+g17DYdfzfEM9DjTuSGu42Db37A8TQHQe8geYEcUZ4SQqZRgzdAI3aRQNlA1yBHJfOeGmOjhLIg==} + + broccoli-config-replace@1.1.3: + resolution: {integrity: sha512-gWGS2h/2VyJnD9tI1/HzRsXLOptnt7tu+KLpfPuxd+DBcdswn/i0kyVrTxQpFy+C5eo2hBn672QAEZzf/7LlAA==} + + broccoli-debug@0.6.5: + resolution: {integrity: sha512-RIVjHvNar9EMCLDW/FggxFRXqpjhncM/3qq87bn/y+/zR9tqEkHvTqbyOc4QnB97NO2m6342w4wGkemkaeOuWg==} + + broccoli-file-creator@1.2.0: + resolution: {integrity: sha512-l9zthHg6bAtnOfRr/ieZ1srRQEsufMZID7xGYRW3aBDv3u/3Eux+Iawl10tAGYE5pL9YB4n5X4vxkp6iNOoZ9g==} + + broccoli-file-creator@2.1.1: + resolution: {integrity: sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + broccoli-filter@1.3.0: + resolution: {integrity: sha512-VXJXw7eBfG82CFxaBDjYmyN7V72D4In2zwLVQJd/h3mBfF3CMdRTsv2L20lmRTtCv1sAHcB+LgMso90e/KYiLw==} + + broccoli-funnel-reducer@1.0.0: + resolution: {integrity: sha512-SaOCEdh+wnt2jFUV2Qb32m7LXyElvFwW3NKNaEJyi5PGQNwxfqpkc0KI6AbQANKgdj/40U2UC0WuGThFwuEUaA==} + + broccoli-funnel@1.2.0: + resolution: {integrity: sha512-0pbFNUA5Ml+gPPd58Rj/M26OS21+bMiV0F+m6+9OVzAhAdppVLxylSsXfWAt2WOD3kS+D8UsDv6GSmnZhbw/dw==} + + broccoli-funnel@2.0.1: + resolution: {integrity: sha512-C8Lnp9TVsSSiZMGEF16C0dCiNg2oJqUKwuZ1K4kVC6qRPG/2Cj/rtB5kRCC9qEbwqhX71bDbfHROx0L3J7zXQg==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + broccoli-funnel@2.0.2: + resolution: {integrity: sha512-/vDTqtv7ipjEZQOVqO4vGDVAOZyuYzQ/EgGoyewfOgh1M7IQAToBKZI0oAQPgMBeFPPlIbfMuAngk+ohPBuaHQ==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + broccoli-funnel@3.0.8: + resolution: {integrity: sha512-ng4eIhPYiXqMw6SyGoxPHR3YAwEd2lr9FgBI1CyTbspl4txZovOsmzFkMkGAlu88xyvYXJqHiM2crfLa65T1BQ==} + engines: {node: 10.* || >= 12.*} + + broccoli-kitchen-sink-helpers@0.3.1: + resolution: {integrity: sha512-gqYnKSJxBSjj/uJqeuRAzYVbmjWhG0mOZ8jrp6+fnUIOgLN6MvI7XxBECDHkYMIFPJ8Smf4xaI066Q2FqQDnXg==} + + broccoli-merge-trees@1.2.4: + resolution: {integrity: sha512-RXJAleytlED0dxXGEo2EXwrg5cCesY8LQzzGRogwGQmluoz+ijzxajpyWAW6wu/AyuQZj1vgnIqnld8jvuuXtQ==} + + broccoli-merge-trees@2.0.1: + resolution: {integrity: sha512-WjaexJ+I8BxP5V5RNn6um/qDRSmKoiBC/QkRi79FT9ClHfldxRyCDs9mcV7mmoaPlsshmmPaUz5jdtcKA6DClQ==} + + broccoli-merge-trees@3.0.2: + resolution: {integrity: sha512-ZyPAwrOdlCddduFbsMyyFzJUrvW6b04pMvDiAQZrCwghlvgowJDY+EfoXn+eR1RRA5nmGHJ+B68T63VnpRiT1A==} + engines: {node: '>=6.0.0'} + + broccoli-merge-trees@4.2.0: + resolution: {integrity: sha512-nTrQe5AQtCrW4enLRvbD/vTLHqyW2tz+vsLXQe4IEaUhepuMGVKJJr+I8n34Vu6fPjmPLwTjzNC8izMIDMtHPw==} + engines: {node: 10.* || >= 12.*} + + broccoli-middleware@2.1.1: + resolution: {integrity: sha512-BK8aPhQpOLsHWiftrqXQr84XsvzUqeaN4PlCQOYg5yM0M+WKAHtX2WFXmicSQZOVgKDyh5aeoNTFkHjBAEBzwQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli-node-api@1.7.0: + resolution: {integrity: sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==} + + broccoli-node-info@1.1.0: + resolution: {integrity: sha512-DUohSZCdfXli/3iN6SmxPbck1OVG8xCkrLx47R25his06xVc1ZmmrOsrThiM8BsCWirwyocODiYJqNP5W2Hg1A==} + engines: {node: '>= 0.10.0'} + + broccoli-node-info@2.2.0: + resolution: {integrity: sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==} + engines: {node: 8.* || >= 10.*} + + broccoli-output-wrapper@2.0.0: + resolution: {integrity: sha512-V/ozejo+snzNf75i/a6iTmp71k+rlvqjE3+jYfimuMwR1tjNNRdtfno+NGNQB2An9bIAeqZnKhMDurAznHAdtA==} + + broccoli-output-wrapper@3.2.5: + resolution: {integrity: sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==} + engines: {node: 10.* || >= 12.*} + + broccoli-persistent-filter@2.3.1: + resolution: {integrity: sha512-hVsmIgCDrl2NFM+3Gs4Cr2TA6UPaIZip99hN8mtkaUPgM8UeVnCbxelCvBjUBHo0oaaqP5jzqqnRVvb568Yu5g==} + engines: {node: 6.* || >= 8.*} + + broccoli-plugin@1.3.1: + resolution: {integrity: sha512-DW8XASZkmorp+q7J4EeDEZz+LoyKLAd2XZULXyD9l4m9/hAKV3vjHmB1kiUshcWAYMgTP1m2i4NnqCE/23h6AQ==} + + broccoli-plugin@2.1.0: + resolution: {integrity: sha512-ElE4caljW4slapyEhSD9jU9Uayc8SoSABWdmY9SqbV8DHNxU6xg1jJsPcMm+cXOvggR3+G+OXAYQeFjWVnznaw==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli-plugin@3.1.0: + resolution: {integrity: sha512-7w7FP8WJYjLvb0eaw27LO678TGGaom++49O1VYIuzjhXjK5kn2+AMlDm7CaUFw4F7CLGoVQeZ84d8gICMJa4lA==} + engines: {node: 8.* || 10.* || >= 12.*} + + broccoli-plugin@4.0.7: + resolution: {integrity: sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==} + engines: {node: 10.* || >= 12.*} + + broccoli-postcss-single@4.0.1: + resolution: {integrity: sha512-WfIKXqkTNyA0OJuJKoUJ2TxerRvtkiwYEK8KY5gHGr/GGqPHYWVnAUN5/O8/LWBNhgr9M94KstpXe+GqXyEflA==} + engines: {node: '>= 10'} + + broccoli-postcss@5.1.0: + resolution: {integrity: sha512-f5cHP5g7EFidu9w88WOLTtbk4dd/W7amK0nek08FkmUII2h4W/Je4EV26HtMEm9nb1hKI301wwuEQ5AQRsVYog==} + engines: {node: '>= 10'} + + broccoli-postcss@6.1.0: + resolution: {integrity: sha512-I8+DHq5xcCBHU0PpCtDMayAmSUVx07CqAquUpdlNUHckXeD//cUFf4aFQllnZBhF8Z86YLhuA+j7qvCYYgBXRg==} + engines: {node: '>= 10'} + + broccoli-replace@2.0.2: + resolution: {integrity: sha512-1e8uyGUo8HqiKKB4oWz5nUX1rlLSRgShLxczuwSXJlmGljVWerDGF0oW5VshGAuKKYkAoDsI3Cc0TKEgo4SWTg==} + engines: {node: '>=10'} + + broccoli-rollup@2.1.1: + resolution: {integrity: sha512-aky/Ovg5DbsrsJEx2QCXxHLA6ZR+9u1TNVTf85soP4gL8CjGGKQ/JU8R3BZ2ntkWzo6/83RCKzX6O+nlNKR5MQ==} + engines: {node: '>=4.0'} + + broccoli-rollup@4.1.1: + resolution: {integrity: sha512-hkp0dB5chiemi32t6hLe5bJvxuTOm1TU+SryFlZIs95KT9+94uj0C8w6k6CsZ2HuIdIZg6D252t4gwOlcTXrpA==} + engines: {node: '>=8.0'} + + broccoli-slow-trees@3.1.0: + resolution: {integrity: sha512-FRI7mRTk2wjIDrdNJd6znS7Kmmne4VkAkl8Ix1R/VoePFMD0g0tEl671xswzFqaRjpT9Qu+CC4hdXDLDJBuzMw==} + + broccoli-source@1.1.0: + resolution: {integrity: sha512-ahvqmwF6Yvh6l+sTJJdey4o4ynwSH8swSSBSGmUXGSPPCqBWvquWB/4rWN65ZArKilBFq/29O0yQnZNIf//sTg==} + + broccoli-source@2.1.2: + resolution: {integrity: sha512-1lLayO4wfS0c0Sj50VfHJXNWf94FYY0WUhxj0R77thbs6uWI7USiOWFqQV5dRmhAJnoKaGN4WyLGQbgjgiYFwQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + broccoli-source@3.0.1: + resolution: {integrity: sha512-ZbGVQjivWi0k220fEeIUioN6Y68xjMy0xiLAc0LdieHI99gw+tafU8w0CggBDYVNsJMKUr006AZaM7gNEwCxEg==} + engines: {node: 8.* || 10.* || >= 12.*} + + broccoli-stew@1.6.0: + resolution: {integrity: sha512-sUwCJNnYH4Na690By5xcEMAZqKgquUQnMAEuIiL3Z2k63mSw9Xg+7Ew4wCrFrMmXMcLpWjZDOm6Yqnq268N+ZQ==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + broccoli-stew@3.0.0: + resolution: {integrity: sha512-NXfi+Vas24n3Ivo21GvENTI55qxKu7OwKRnCLWXld8MiLiQKQlWIq28eoARaFj0lTUFwUa4jKZeA7fW9PiWQeg==} + engines: {node: 8.* || >= 10.*} + + broccoli-string-replace@0.1.2: + resolution: {integrity: sha512-QHESTrrrPlKuXQNWsvXawSQbV2g34wCZ5oKgd6bntdOuN8VHxbg1BCBHqVY5HxXJhWelimgGxj3vI7ECkyij8g==} + + broccoli-svg-optimizer@2.1.0: + resolution: {integrity: sha512-fGB4WUF8R9tHUf6M2t8F38ILLdVy+CQVaOFwHavaaXPD0kkoTsHjBE7erQZuk0PrioqLIoyA9dkeNMlGwohReA==} + engines: {node: 12.* || 14.* || >= 16} + + broccoli-templater@2.0.2: + resolution: {integrity: sha512-71KpNkc7WmbEokTQpGcbGzZjUIY1NSVa3GB++KFKAfx5SZPUozCOsBlSTwxcv8TLoCAqbBnsX5AQPgg6vJ2l9g==} + engines: {node: 6.* || >= 8.*} + + broccoli-terser-sourcemap@4.1.1: + resolution: {integrity: sha512-8sbpRf0/+XeszBJQM7vph2UNj4Kal0lCI/yubcrBIzb2NvYj5gjTHJABXOdxx5mKNmlCMu2hx2kvOtMpQsxrfg==} + engines: {node: ^10.12.0 || 12.* || >= 14} + + broccoli@3.5.2: + resolution: {integrity: sha512-sWi3b3fTUSVPDsz5KsQ5eCQNVAtLgkIE/HYFkEZXR/07clqmd4E/gFiuwSaqa9b+QTXc1Uemfb7TVWbEIURWDg==} + engines: {node: 8.* || >= 10.*} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browser-assert@1.2.1: + resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} + + browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + + browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + + browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + + browserify-fs@1.0.0: + resolution: {integrity: sha512-8LqHRPuAEKvyTX34R6tsw4bO2ro6j9DmlYBhiYWHRM26Zv2cBw1fJOU0NeUQ0RkXkPn/PFBjhA0dm4AgaBurTg==} + + browserify-rsa@4.1.1: + resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==} + engines: {node: '>= 0.10'} + + browserify-sign@4.2.5: + resolution: {integrity: sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==} + engines: {node: '>= 0.10'} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + browserslist@3.2.8: + resolution: {integrity: sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==} + hasBin: true + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + brute-knex@4.0.1: + resolution: {integrity: sha512-rV2tY8amv+2ERYNNC7voCl1A4Mh+s2IvyyDo3DAMKhaR4ME8r+4t9MH0Fgqjpe1ievESYX9Pes7gf05LBBUCRA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + bson-objectid@2.0.4: + resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==} + + bthreads@0.5.1: + resolution: {integrity: sha512-nK7Jo9ll+r1FRMNPWEFRTZMQrX6HhX8JjPAofxmbTNILHqWVIJPmWzCi9JlX/K0DL5AKZTFZg2Qser5C6gVs9A==} + engines: {node: '>=8.0.0'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-es6@4.9.3: + resolution: {integrity: sha512-Ibt+oXxhmeYJSsCkODPqNpPmyegefiD8rfutH1NYGhMZQhSp95Rz7haemgnJ6dxa6LT+JLLbtgOMORRluwKktw==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + + buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufio@1.0.7: + resolution: {integrity: sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A==} + engines: {node: '>=8.0.0'} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + + builtins@1.0.3: + resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bunyan-loggly@2.0.1: + resolution: {integrity: sha512-9F+BYVqqtrYHmWfDE2JLoQmQmZod7AQQuRtAS41qaXa5wjiOviFNnMlY56gZmv5SZ2czbeuWeX+xSAEFcktsXw==} + + bunyan@1.8.15: + resolution: {integrity: sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==} + engines: {'0': node >=0.10.0} + hasBin: true + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + byte-counter@0.1.0: + resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==} + engines: {node: '>=20'} + + bytes@1.0.0: + resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c8@10.1.3: + resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@12.0.4: + resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + cache-manager-ioredis@2.1.0: + resolution: {integrity: sha512-TCxbp9ceuFveTKWuNaCX8QjoC41rAlHen4s63u9Yd+iXlw3efYmimc/u935PKPxSdhkXpnMes4mxtK3/yb0L4g==} + engines: {node: '>=6.0.0'} + + cache-manager@4.1.0: + resolution: {integrity: sha512-ZGM6dLxrP65bfOZmcviWMadUOCICqpLs92+P/S5tj8onz+k+tB7Gr+SAgOUHCQtfm2gYEQDHiKeul4+tYPOJ8A==} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + cacheable-request@13.0.18: + resolution: {integrity: sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==} + engines: {node: '>=18'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + calculate-cache-key-for-tree@2.0.0: + resolution: {integrity: sha512-Quw8a6y8CPmRd6eU+mwypktYCwUcf8yVFIRbNZ6tPQEckX9yd+EBVEPC/GSZZrMWH9e7Vz4pT7XhpmyApRByLQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caller-callsite@2.0.0: + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} + + caller-path@2.0.0: + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} + + caller@1.1.0: + resolution: {integrity: sha512-n+21IZC3j06YpCWaxmUy5AnVqhmCIM2bQtqQyy00HJlmStRt6kwDX5F9Z97pqwAB+G/tgSz6q/kUBbNyQzIubw==} + + callsites@2.0.0: + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + can-symlink@1.0.0: + resolution: {integrity: sha512-RbsNrFyhwkx+6psk/0fK/Q9orOUr9VMxohGd8vTa4djf4TGLfblBgUfqZChrZuW0Q+mz2eBPFLusw9Jfukzmhg==} + hasBin: true + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001781: + resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + + capture-exit@2.0.0: + resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} + engines: {node: 6.* || 8.* || >= 10.*} + + cardinal@1.0.0: + resolution: {integrity: sha512-INsuF4GyiFLk8C91FPokbKTc/rwHqV4JnfatVZ6GPhguP1qmkRWX2dp5tepYboYdPpGWisLVLI+KsXoXFPRSMg==} + hasBin: true + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + ccount@1.1.0: + resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} + + chai-dom@1.12.1: + resolution: {integrity: sha512-tvz+D0PJue2VHXRec3udgP/OeeXBiePU3VH6JhEnHQJYzvNzR2nUvEykA9dXVS76JvaUENSOYH8Ufr0kZSnlCQ==} + engines: {node: '>= 0.12.0'} + peerDependencies: + chai: '>= 3' + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + character-entities-html4@1.1.4: + resolution: {integrity: sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==} + + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + charm@1.0.2: + resolution: {integrity: sha512-wqW3VdPnlSWT4eRiYX+hcs+C6ViBPUWk1qTCd+37qw9kEm/a5n2qcyQDMBWvSYKN/ctqZzeXNQaeBjOetJJUkw==} + + charset@1.0.1: + resolution: {integrity: sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==} + engines: {node: '>=4.0.0'} + + chart.js@2.9.4: + resolution: {integrity: sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==} + + chartjs-color-string@0.6.0: + resolution: {integrity: sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==} + + chartjs-color@2.4.1: + resolution: {integrity: sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@0.22.0: + resolution: {integrity: sha512-8/MzidM6G/TgRelkzDG13y3Y9LxBjCb+8yOEZ9+wwq5gVF2w2pV0wmHvjfT0RvuxGyR7UEuK36r+yYMbT4uKgA==} + engines: {node: '>= 0.6'} + + cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chokidar@2.1.8: + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + chrono-node@2.9.0: + resolution: {integrity: sha512-glI4YY2Jy6JII5l3d5FN6rcrIbKSQqKPhWsIRYPK2IK8Mm4Q1ZZFdYIaDqglUNf7gNwG+kWIzTn0omzzE0VkvQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clean-base-url@1.0.0: + resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} + + clean-css-promise@0.1.1: + resolution: {integrity: sha512-tzWkANXMD70ETa/wAu2TXAAxYWS0ZjVUFM2dVik8RQBoAbGMFJv4iVluz3RpcoEbo++fX4RV/BXfgGoOjp8o3Q==} + + clean-css@3.4.28: + resolution: {integrity: sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==} + engines: {node: '>=0.10.0'} + hasBin: true + + clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + + clean-regexp@1.0.0: + resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} + engines: {node: '>=4'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + clean-stack@3.0.1: + resolution: {integrity: sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==} + engines: {node: '>=10'} + + clean-up-path@1.0.0: + resolution: {integrity: sha512-PHGlEF0Z6976qQyN6gM7kKH6EH0RdfZcc8V+QhFe36eRxV0SMH5OUBZG7Bxa9YcreNzyNbK63cGiZxdSZgosRw==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-table@0.3.11: + resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} + engines: {node: '>= 0.2.0'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@0.1.19: + resolution: {integrity: sha512-IO78I0y6JcSpEPHzK4obKdsL7E7oLdRVDVOLwr2Hkbjsb+Eoz0dxW6tef0WizoKu0gLC4oZSZuEF4U2K6w1WQw==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + coa@2.0.2: + resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==} + engines: {node: '>= 4.0'} + + code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + + codemirror@5.48.2: + resolution: {integrity: sha512-i9VsmC8AfA5ji6EDIZ+aoSe4vt9FcwPLdHB1k1ItFbVyuOFRrcfvnoKqwZlC7EVA2UmTRiNEypE4Uo7YvzVY8Q==} + + coffeescript@1.12.7: + resolution: {integrity: sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA==} + engines: {node: '>=0.8.0'} + hasBin: true + + collapse-white-space@1.0.6: + resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@1.1.0: + resolution: {integrity: sha512-6S062WDQUXi6hOfkO/sBPVwE5ASXY4G2+b4atvhJfSsuUUhIaUKlkjLe9692Ipyt5/a+IPF5aVTu3V5gvXq5cg==} + + colorette@1.2.1: + resolution: {integrity: sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colors@1.0.3: + resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} + engines: {node: '>=0.1.90'} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + combine-errors@3.0.3: + resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@0.6.1: + resolution: {integrity: sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==} + engines: {node: '>= 0.4.x'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@2.3.0: + resolution: {integrity: sha512-CD452fnk0jQyk3NfnK+KkR/hUPoHt5pVaKHogtyyv3N0U4QfAal9W0/rXLOg/vVZgQKa7jdtXypKs1YAip11uQ==} + engines: {node: '>= 0.6.x'} + + commander@2.8.1: + resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} + engines: {node: '>= 0.6.x'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compare-ver@2.0.2: + resolution: {integrity: sha512-VeznF8KOp4C6rSg22tvnk8vgAveEMxVVgOVuCzqYIzGyzD2hQ4Zm/O5RKfOECcTqmn0BAAkyJKAN0eqkgUvsEA==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + + condense-whitespace@2.0.0: + resolution: {integrity: sha512-Ath9o58/0rxZXbyoy3zZgrVMoIemi30sukG/btuMKCLyqfQt3dNOWc9N3EHEMa2Q3i0tXQPDJluYFLwy7pJuQw==} + engines: {node: '>=8'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + + connect-slashes@1.4.0: + resolution: {integrity: sha512-BJRbgSczzlsRwyF64DxGNIizBTxUf7f/tAsDzq2Nq8eLrm2160vVfm/4vQcjrT4qVFu6qDCqPK+vDaEWJsnSzA==} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + console-ui@3.1.2: + resolution: {integrity: sha512-+5j3R4wZJcEYZeXk30whc4ZU/+fWW9JMTNntVuMYpjZJ9n26Cxr0tUBXco1NRjVZRpRVvZ4DDKKKIHNYeUG9Dw==} + engines: {node: 6.* || 8.* || >= 10.*} + + consolidate@0.15.1: + resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} + engines: {node: '>= 0.10.0'} + deprecated: Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog + peerDependencies: + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + babel-core: ^6.26.3 + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jade: ^1.11.0 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + marko: ^3.14.4 + mote: ^0.2.0 + mustache: ^3.0.0 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + razor-tmpl: ^1.3.1 + react: ^16.13.1 + react-dom: ^16.13.1 + slm: ^2.0.0 + squirrelly: ^5.1.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-jade: '*' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + arc-templates: + optional: true + atpl: + optional: true + babel-core: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jade: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + marko: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + razor-tmpl: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + squirrelly: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-jade: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + + consolidate@1.0.4: + resolution: {integrity: sha512-RuZ3xnqEDsxiwaoIkqVeeK3gg9qxw7+YKYX2tKhLs1eukVKMgSr4VYI3iYFsRHi4TloHYDlugrz3kvkjs3nynA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.22.5 + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + mote: ^0.2.0 + mustache: ^4.0.1 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + react: '>=16.13.1' + react-dom: '>=16.13.1' + slm: ^2.0.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + '@babel/core': + optional: true + arc-templates: + optional: true + atpl: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + + constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-tag@2.0.3: + resolution: {integrity: sha512-htLIdtfhhKW2fHlFLnZH7GFzHSdSpHhDLrWVswkNiiPMZ5uXq5JfrGboQKFhNQuAAFF8VNB2EYUj3MsdJrKKpg==} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + continuable-cache@0.3.1: + resolution: {integrity: sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-session@2.1.1: + resolution: {integrity: sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==} + engines: {node: '>= 0.10'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + copy-concurrently@1.0.5: + resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} + deprecated: This package is no longer supported. + + copy-dereference@1.0.0: + resolution: {integrity: sha512-40TSLuhhbiKeszZhK9LfNdazC67Ue4kq/gGwN5sdxEUWPXTIMmKmGmgD9mPfNKVAeecEW+NfEIpBaZoACCQLLw==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-object@3.1.5: + resolution: {integrity: sha512-sA2/4+/PZ/KV6CKgjrVrrUVBKCkdDO02CUlQ0YKTQoYUwPYNOtOAcWlbYhd5v/1JqYaA6oZ4sDlOU4ppVw6Wbg==} + engines: {node: '>= 4'} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@5.2.1: + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + countries-and-timezones@3.8.0: + resolution: {integrity: sha512-+Ze9h5f4dQpUwbzTm0DEkiPiZyim9VHV4/mSnT4zNYJnrnfwsKjAZPtnp7J5VzejCDgySs+2SSc6MDdCnD43GA==} + engines: {node: '>=8.x', npm: '>=5.x'} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + + create-error@0.3.1: + resolution: {integrity: sha512-n/Q4aSCtYuuDneEW5Q+nd0IIZwbwmX/oF6wKcDUhXGJNwhmp2WHEoWKz7X+/H7rBtjimInW7f0ceouxU0SmuzQ==} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cron-validate@1.4.5: + resolution: {integrity: sha512-nKlOJEnYKudMn/aNyNH8xxWczlfpaazfWV32Pcx/2St51r2bxWbGhZD7uwzMcRhunA/ZNL+Htm/i0792Z59UMQ==} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-browserify@3.12.1: + resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} + engines: {node: '>= 0.10'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + crypto@0.0.3: + resolution: {integrity: sha512-Q6Ka98WcvWXXg+9cnqd3jHpTSIOaH6/q0m/bESMfQo/0uFxy6e/7EqVS4JdaWx9qLdqV56tDufy2b12dj7BHJg==} + deprecated: This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in. + + cson-parser@4.0.9: + resolution: {integrity: sha512-I79SAcCYquWnEfXYj8hBqOOWKj6eH6zX1hhX3yqmS4K3bYp7jME3UFpHPzu3rUew0oyfc0s8T6IlWGXRAheHag==} + engines: {node: '>=10.13'} + + css-blank-pseudo@3.0.3: + resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + + css-color-names@0.0.4: + resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==} + + css-declaration-sorter@4.0.1: + resolution: {integrity: sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==} + engines: {node: '>4'} + + css-declaration-sorter@7.3.1: + resolution: {integrity: sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-functions-list@3.3.3: + resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} + engines: {node: '>=12'} + + css-has-pseudo@3.0.4: + resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + + css-line-break@1.1.1: + resolution: {integrity: sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==} + + css-loader@5.2.7: + resolution: {integrity: sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.27.0 || ^5.0.0 + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-prefers-color-scheme@6.0.3: + resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + + css-select-base-adapter@0.1.1: + resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==} + + css-select@1.2.0: + resolution: {integrity: sha512-dUQOBoqdR7QwV90WysXPLXG5LO7nhYBgiWVfxF80DKPF8zx1t/pUd2FYy73emg3zrjtM6dzmYgbHKfV2rxiHQA==} + + css-select@2.1.0: + resolution: {integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@1.0.0-alpha.29: + resolution: {integrity: sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==} + engines: {node: '>=0.10.0'} + + css-tree@1.0.0-alpha.33: + resolution: {integrity: sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w==} + engines: {node: '>=0.10.0'} + + css-tree@1.0.0-alpha.37: + resolution: {integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==} + engines: {node: '>=8.0.0'} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@2.1.3: + resolution: {integrity: sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==} + + css-what@3.4.2: + resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} + engines: {node: '>= 6'} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssdb@7.11.2: + resolution: {integrity: sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-default@4.0.8: + resolution: {integrity: sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==} + engines: {node: '>=6.9.0'} + + cssnano-preset-default@7.0.11: + resolution: {integrity: sha512-waWlAMuCakP7//UCY+JPrQS1z0OSLeOXk2sKWJximKWGupVxre50bzPlvpbUwZIDylhf/ptf0Pk+Yf7C+hoa3g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + cssnano-util-get-arguments@4.0.0: + resolution: {integrity: sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==} + engines: {node: '>=6.9.0'} + + cssnano-util-get-match@4.0.0: + resolution: {integrity: sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==} + engines: {node: '>=6.9.0'} + + cssnano-util-raw-cache@4.0.1: + resolution: {integrity: sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==} + engines: {node: '>=6.9.0'} + + cssnano-util-same-parent@4.0.1: + resolution: {integrity: sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==} + engines: {node: '>=6.9.0'} + + cssnano-utils@5.0.1: + resolution: {integrity: sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + cssnano@4.1.10: + resolution: {integrity: sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==} + engines: {node: '>=6.9.0'} + + cssnano@7.1.1: + resolution: {integrity: sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + csso@3.5.1: + resolution: {integrity: sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==} + engines: {node: '>=0.10.0'} + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.4.4: + resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + csv-writer@1.6.0: + resolution: {integrity: sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==} + + custom-error-instance@2.1.1: + resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} + + cyclist@1.0.2: + resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + dag-map@2.0.2: + resolution: {integrity: sha512-xnsprIzYuDeiyu5zSKwilV/ajRHxnoMlAhEREfyfTgTSViMVY2fGP1ZcHJbtwup26oCkofySU/m6oKJ3HrkW7w==} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-uri-to-buffer@5.0.1: + resolution: {integrity: sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==} + engines: {node: '>= 14'} + + data-uri-utils@1.0.12: + resolution: {integrity: sha512-TvKeyxno5JjVbUhjTY3C8dfBOPpn5nN7Hs2StbsrvBEqhA/n3sc3K2cwja2Le5JXtiwxxPiIn8GrW+qzUy60yg==} + engines: {node: '>= 14'} + + data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + date-format@4.0.14: + resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} + engines: {node: '>=4.0'} + + date-time@2.1.0: + resolution: {integrity: sha512-/9+C44X7lot0IeiyfgJmETtRMhBidBYM2QFFIkGa0U1k+hSyY87Nw7PY3eDqpvCBm7I3WCSfPeZskW/YYq6m4g==} + engines: {node: '>=4'} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug-logfmt@1.4.10: + resolution: {integrity: sha512-+8rNw7zjXNRntMoJyp5211Y4W3nkhCCMBO7qe8Pht/9NscMklHwyTXMLUzk84YUDSksg87XRmK/LCzJdJ4eU7Q==} + engines: {node: '>= 8'} + + debug@2.2.0: + resolution: {integrity: sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.1.1: + resolution: {integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==} + deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.1: + resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decamelize@5.0.1: + resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} + engines: {node: '>=10'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + decompress-response@10.0.0: + resolution: {integrity: sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==} + engines: {node: '>=20'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + decorator-transforms@2.3.1: + resolution: {integrity: sha512-PDOk74Zqqy0946Lx4ckXxbgG6uhPScOICtrxL/pXmfznxchqNee0TaJISClGJQe6FeT8ohGqsOgdjfahm4FwEw==} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + deferred-leveldown@0.2.0: + resolution: {integrity: sha512-+WCbb4+ez/SZ77Sdy1iadagFiVzMB89IKOBhglgnUkVxOxRWmmFsz8UDSNWh4Rhq+3wr/vMFlYj+rdEwWUDdng==} + deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@1.5.1: + resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} + engines: {node: '>=0.10'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + detect-indent@4.0.0: + resolution: {integrity: sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==} + engines: {node: '>=0.10.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-sequences@28.1.1: + resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@1.4.0: + resolution: {integrity: sha512-VzVc42hMZbYU9Sx/ltb7KYuQ6pqAw+cbFWVy4XKdkuEL2CFaRLGEnISPs7YdzaUGpi+CpIqvRmu7hPQ4T7EQ5w==} + engines: {node: '>=0.3.1'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + discontinuous-range@1.0.0: + resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@4.0.10: + resolution: {integrity: sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==} + engines: {node: '>= 8.0'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dom-serializer@0.1.1: + resolution: {integrity: sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domain-browser@1.2.0: + resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} + engines: {node: '>=0.4', npm: '>=1.2'} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead + + domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + + domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + domutils@1.5.1: + resolution: {integrity: sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==} + + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + downsize@0.0.8: + resolution: {integrity: sha512-6vNP5DESwCTwefIFlMUp7MX6GfvjNYWjoLIdfdqPcYPNUcKKfjUctw2yMNbyM1wlkgfCO0oydSR/eysDHFo60w==} + + dtrace-provider@0.8.8: + resolution: {integrity: sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==} + engines: {node: '>=0.10'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + + editions@1.3.4: + resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} + engines: {node: '>=0.8'} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + ee-argv@0.1.4: + resolution: {integrity: sha512-L/bJ7iteO7JlaOmjwWObBAoy2XnqP9sgazSE25xLenBmkN6HELbcZJISBFwvixL8re7HgCn2KrwuC/Ai0/s6hQ==} + engines: {node: '>=0.10.7'} + + ee-class@1.4.0: + resolution: {integrity: sha512-0rHVsCh/QNXHkX4HEnh9IR2/VDPc2KoOjvbSp3BqxH/fbvHtniteQOCjYWCTYvkfQj/KRSTcc/eXP7z/Ic20mg==} + engines: {node: '>=v4.2.1'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ee-log@1.1.0: + resolution: {integrity: sha512-OCVidQvuHmnhQB4A1zb50MFAwCZvHIOpGhCL0FBkUAUx7RQcH+YE+lfklddx5dUjr3QgD14TCPCGemT9H98V4Q==} + engines: {node: '>4'} + + ee-log@3.0.9: + resolution: {integrity: sha512-SdIG4RfLPuv8N2cIJWgaW00V6xZsrEZJq/OcKl1SkyeNYYGeQKOUS0JzyvTAiPvy8bLCLSLYwtx37ZUQPe1xJg==} + engines: {node: '>=8'} + + ee-types@2.2.1: + resolution: {integrity: sha512-ZgShE8RXsE+DFAddCmduKwUwoNLZd7Ik6yv6LFEUDfz/6k2s6rTvABQS8dO2EibJpYFWREOx/ealtwuTUXeeYg==} + + electron-to-chromium@1.5.328: + resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + + element-closest@2.0.2: + resolution: {integrity: sha512-QCqAWP3kwj8Gz9UXncVXQGdrhnWxD8SQBSeZp5pOsyCcQ6RpL738L1/tfuwBiMi6F1fYkxqPnBrFBR4L+f49Cg==} + engines: {node: '>=4.0.0'} + + element-resize-detector@1.2.4: + resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==} + + elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + + ember-ajax@5.1.2: + resolution: {integrity: sha512-4pMrrJjZGpEGQEChWZ1+YO9TZocpjUVk2VdNv6nBgkU+C3UOYUkdzyeinWz5JLmRTL0ktrA6/EU9H+H0hsMgsQ==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + ember-assign-helper@0.2.0: + resolution: {integrity: sha512-WO6K24m6Wk+G1PBOMaXUtOMkzCTtt9z67SPIcAFxOVqc95hUU62wDRUdDiPa/854T5rroq5CJ7Ey4LgxorCsgg==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-assign-helper@0.4.0: + resolution: {integrity: sha512-GKHhT4HD2fhtDnuBk6eCdCA8XGew9hY7TVs8zjrykegiI7weC0CGtpJscmIG3O0gEEb0d07UTkF2pjfNGLx4Nw==} + engines: {node: '>= 12'} + + ember-assign-helper@0.5.0: + resolution: {integrity: sha512-swH7FqmqB5iSeoKlU6X41iqw5HQ+EdBDyFDXmwytTyUd5GRvfGfZUn2SMUUGdyvo5FxXJWqMJ0rBT//EcGC0+Q==} + peerDependencies: + ember-source: ^3.28.0 || ^4.0.0 || >=5.0.0 + + ember-assign-polyfill@2.7.3: + resolution: {integrity: sha512-PINAtHOQf5DKniyecOBZSz8VZVmtIKFvp67853+aw+TL+LWUCji5OjQ13PrAV/GIl3Fp2sZ7IbEPyTobDL7Y8Q==} + engines: {node: 6.* || 8.* || 10.* || >= 12} + + ember-auto-import@1.12.2: + resolution: {integrity: sha512-gLqML2k77AuUiXxWNon1FSzuG1DV7PEPpCLCU5aJvf6fdL6rmFfElsZRh+8ELEB/qP9dT+LHjNEunVzd2dYc8A==} + engines: {node: '>= 10.*'} + + ember-auto-import@2.10.0: + resolution: {integrity: sha512-bcBFDYVTFHyqyq8BNvsj6UO3pE6Uqou/cNmee0WaqBgZ+1nQqFz0UE26usrtnFAT+YaFZSkqF2H36QW84k0/cg==} + engines: {node: 12.* || 14.* || >= 16} + + ember-basic-dropdown@6.0.2: + resolution: {integrity: sha512-JgI/cy7eS/Y2WoQl7B2Mko/1aFTAlxr5d+KpQeH7rBKOFml7IQtLvhiDQrpU/FLkrQ9aLNEJtzwtDZV1xQxAKA==} + engines: {node: 12.* || 14.* || >= 16} + + ember-classic-decorator@3.0.1: + resolution: {integrity: sha512-IoocHPX1cY93Sma7VeDGbF0M1+TkBWDJLus+RD01902eD+KHEL2+diwM+iayCyO+1/xaUzdzNGh35J+i+TCxoA==} + engines: {node: 12.* || 14.* || >= 16} + + ember-cli-app-version@5.0.0: + resolution: {integrity: sha512-afhx/CXDOMNXzoe4NDPy5WUfxWmYYHUzMCiTyvPBxCDBXYcMrtxNWxvgaSaeqcoHVEmqzeyBj8V82tzmT1dcyw==} + engines: {node: 10.* || >= 12} + + ember-cli-babel-plugin-helpers@1.1.1: + resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-babel@6.18.0: + resolution: {integrity: sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-cli-babel@7.26.11: + resolution: {integrity: sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-babel@8.2.0: + resolution: {integrity: sha512-8H4+jQElCDo6tA7CamksE66NqBXWs7VNpS3a738L9pZCjg2kXIX4zoyHzkORUqCtr0Au7YsCnrlAMi1v2ALo7A==} + engines: {node: 16.* || 18.* || >= 20} + peerDependencies: + '@babel/core': ^7.12.0 + + ember-cli-chart@3.7.2: + resolution: {integrity: sha512-mtzJ6XYzoBwvEZUws5K5kFekVPOfcB6PE42O4/2I82k/+du9GidEOoOJ6ZXFCJPIdrxKZYo1F1IWE7pVa6m9Ig==} + engines: {node: 10.* || >= 12} + + ember-cli-code-coverage@1.0.3: + resolution: {integrity: sha512-tyWeQ22vxpDmfhIrRCMqZPq9Coppefg19hBgME4yb9Na2qslxCNK0USThigZhesb7hfw2ZgdrKJCrmCVNwkq7g==} + engines: {node: 10.* || >= 12} + + ember-cli-dependency-checker@3.3.2: + resolution: {integrity: sha512-PwkrW5oYsdPWwt+0Tojufmv/hxVETTjkrEdK7ANQB2VSnqpA5UcYubwpQM9ONuR2J8wyNDMwEHlqIrk/FYtBsQ==} + engines: {node: '>= 6'} + peerDependencies: + ember-cli: ^3.2.0 || >=4.0.0 + + ember-cli-deprecation-workflow@2.2.0: + resolution: {integrity: sha512-23bXZqZJBJSKBTfT0LK7qzSJX861TgafL6RVdMfn/iubpLnoZIWergYwEdgs24CNTUbuehVbHy2Q71o8jYfwfw==} + engines: {node: 12.* || 14.* || >= 16} + + ember-cli-element-closest-polyfill@0.0.1: + resolution: {integrity: sha512-LBYICCsG1WIhsieUoZndW5LQyuz7DtnXYgGrH9APuwR7qYsEdXJt5Uv6Pvu211GY2EnuNtJ7V24ZhFGCFu1dQg==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-get-component-path-option@1.0.0: + resolution: {integrity: sha512-k47TDwcJ2zPideBCZE8sCiShSxQSpebY2BHcX2DdipMmBox5gsfyVrbKJWIHeSTTKyEUgmBIvQkqTOozEziCZA==} + + ember-cli-htmlbars-inline-precompile@2.1.0: + resolution: {integrity: sha512-BylIHduwQkncPhnj0ZyorBuljXbTzLgRo6kuHf1W+IHFxThFl2xG+r87BVwsqx4Mn9MTgW9SE0XWjwBJcSWd6Q==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: Use ember-cli-htmlbars instead. + peerDependencies: + ember-cli-babel: ^6.7.1 || ^7.0.0 + + ember-cli-htmlbars@3.1.0: + resolution: {integrity: sha512-cgvRJM73IT0aePUG7oQ/afB7vSRBV3N0wu9BrWhHX2zkR7A7cUBI7KC9VPk6tbctCXoM7BRGsCC4aIjF7yrfXA==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-htmlbars@4.5.0: + resolution: {integrity: sha512-bYJpK1pqFu9AadDAGTw05g2LMNzY8xTCIqQm7dMJmKEoUpLRFbPf4SfHXrktzDh7Q5iggl6Skzf1M0bPlIxARw==} + engines: {node: 8.* || 10.* || >= 12.*} + + ember-cli-htmlbars@5.7.2: + resolution: {integrity: sha512-Uj6R+3TtBV5RZoJY14oZn/sNPnc+UgmC8nb5rI4P3fR/gYoyTFIZSXiIM7zl++IpMoIrocxOrgt+mhonKphgGg==} + engines: {node: 10.* || >= 12.*} + + ember-cli-htmlbars@6.3.0: + resolution: {integrity: sha512-N9Y80oZfcfWLsqickMfRd9YByVcTGyhYRnYQ2XVPVrp6jyUyOeRWmEAPh7ERSXpp8Ws4hr/JB9QVQrn/yZa+Ag==} + engines: {node: 12.* || 14.* || >= 16} + + ember-cli-inject-live-reload@2.1.0: + resolution: {integrity: sha512-YV5wYRD5PJHmxaxaJt18u6LE6Y+wo455BnmcpN+hGNlChy2piM9/GMvYgTAz/8Vin8RJ5KekqP/w/NEaRndc/A==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-is-package-missing@1.0.0: + resolution: {integrity: sha512-9hEoZj6Au5onlSDdcoBqYEPT8ehlYntZPxH8pBKV0GO7LNel88otSAQsCfXvbi2eKE+MaSeLG/gNaCI5UdWm9g==} + + ember-cli-lodash-subset@2.0.1: + resolution: {integrity: sha512-QkLGcYv1WRK35g4MWu/uIeJ5Suk2eJXKtZ+8s+qE7C9INmpCPyPxzaqZABquYzcWNzIdw6kYwz3NWAFdKYFxwg==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-cli-mirage@2.4.0: + resolution: {integrity: sha512-cy8B+IZV07V6xgnFzktKUsntTQvIqPSS3u4+XaLdNW91yOowLsN2BsuQldN3eCnwswgE3a9eGNGS4I0BD4llNA==} + engines: {node: '>= 10.*'} + peerDependencies: + '@ember/test-helpers': '*' + ember-data: '*' + ember-qunit: '*' + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + ember-data: + optional: true + ember-qunit: + optional: true + + ember-cli-node-assets@0.2.2: + resolution: {integrity: sha512-pFyjlhzwx2FxAmkxSVJvP+i+MwHDhmgsmma1ZQbFLYwBeufo1GIzqSJUfStcpOE1NDg8fXm2yZVVzdZYf9lW2w==} + engines: {node: '>= 4'} + + ember-cli-normalize-entity-name@1.0.0: + resolution: {integrity: sha512-rF4P1rW2P1gVX1ynZYPmuIf7TnAFDiJmIUFI1Xz16VYykUAyiOCme0Y22LeZq8rTzwBMiwBwoE3RO4GYWehXZA==} + + ember-cli-path-utils@1.0.0: + resolution: {integrity: sha512-Qq0vvquzf4cFHoDZavzkOy3Izc893r/5spspWgyzLCPTaG78fM3HsrjZm7UWEltbXUqwHHYrqZd/R0jS08NqSA==} + + ember-cli-postcss@6.0.1: + resolution: {integrity: sha512-xnqcFE9/OdfnxqisLHys7VZLzzSSGg96fzTCL+E/q2RqvFmtUC9FRP1sXtt7/UCogB1m4cdqYeW/a0VD8zBgPg==} + engines: {node: '>= 10'} + + ember-cli-preprocess-registry@3.3.0: + resolution: {integrity: sha512-60GYpw7VPeB7TvzTLZTuLTlHdOXvayxjAQ+IxM2T04Xkfyu75O2ItbWlftQW7NZVGkaCsXSRAmn22PG03VpLMA==} + + ember-cli-shims@1.2.0: + resolution: {integrity: sha512-eqoRtBl4i27uyIvopQqJxWNy8PFCDoePWf6oqf+ICR75I1fx3jTvgatCpUUL5E44DENwyDYWgoIqL/KAO+UFwg==} + engines: {node: ^4.5 || 6.* || >= 7.*} + deprecated: 'See the README about migration instructions: https://github.com/ember-cli/ember-cli-shims' + + ember-cli-string-helpers@6.1.0: + resolution: {integrity: sha512-Lw8B6MJx2n8CNF2TSIKs+hWLw0FqSYjr2/NRPyquyYA05qsl137WJSYW3ZqTsLgoinHat0DGF2qaCXocLhLmyA==} + engines: {node: 10.* || >=12.*} + + ember-cli-string-utils@1.1.0: + resolution: {integrity: sha512-PlJt4fUDyBrC/0X+4cOpaGCiMawaaB//qD85AXmDRikxhxVzfVdpuoec02HSiTGTTB85qCIzWBIh8lDOiMyyFg==} + + ember-cli-terser@4.0.1: + resolution: {integrity: sha512-vvp0uVl8reYeW9EZjSXRPR3Bq7y4u9CYlUdI7j/WzMPDj3/gUHU4Z7CHYOCrftrClQvFfqO2eXmHwDA6F7SLug==} + engines: {node: 10.* || 12.* || >= 14} + + ember-cli-test-info@1.0.0: + resolution: {integrity: sha512-dEVTIpmUfCzweC97NGf6p7L6XKBwV2GmSM4elmzKvkttEp5P7AvGA9uGyN4GqFq+RwhW+2b0I2qlX00w+skm+A==} + + ember-cli-test-loader@2.2.0: + resolution: {integrity: sha512-mlSXX9SciIRwGkFTX6XGyJYp4ry6oCFZRxh5jJ7VH8UXLTNx2ZACtDTwaWtNhYrWXgKyiDUvmD8enD56aePWRA==} + engines: {node: '>= 4.0'} + + ember-cli-test-loader@3.1.0: + resolution: {integrity: sha512-0aocZV9SIoOHiU3hrH3IuLR6busWhTX6UVXgd490hmJkIymmOXNH2+jJoC7Ebkeo3PiOfAdjqhb765QDlHSJOw==} + engines: {node: 10.* || >= 12} + + ember-cli-typescript@2.0.2: + resolution: {integrity: sha512-7I5azCTxOgRDN8aSSnJZIKSqr+MGnT+jLTUbBYqF8wu6ojs2DUnTePxUcQMcvNh3Q3B1ySv7Q/uZFSjdU9gSjA==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-typescript@3.0.0: + resolution: {integrity: sha512-lo5YArbJzJi5ssvaGqTt6+FnhTALnSvYVuxM7lfyL1UCMudyNJ94ovH5C7n5il7ATd6WsNiAPRUO/v+s5Jq/aA==} + engines: {node: 8.* || >= 10.*} + + ember-cli-typescript@3.1.4: + resolution: {integrity: sha512-HJ73kL45OGRmIkPhBNFt31I1SGUvdZND+LCH21+qpq3pPlFpJG8GORyXpP+2ze8PbnITNLzwe5AwUrpyuRswdQ==} + engines: {node: 8.* || >= 10.*} + + ember-cli-typescript@4.2.1: + resolution: {integrity: sha512-0iKTZ+/wH6UB/VTWKvGuXlmwiE8HSIGcxHamwNhEC5x1mN3z8RfvsFZdQWYUzIWFN2Tek0gmepGRPTwWdBYl/A==} + engines: {node: 10.* || >= 12.*} + + ember-cli-typescript@5.3.0: + resolution: {integrity: sha512-gFA+ZwmsvvFwo2Jz/B9GMduEn+fPoGb69qWGP0Tp3+Tu5xypDtIKVSZ5086I3Cr19cLXD4HkrOR3YQvdUKzAkQ==} + engines: {node: '>= 12.*'} + + ember-cli-version-checker@2.2.0: + resolution: {integrity: sha512-G+KtYIVlSOWGcNaTFHk76xR4GdzDLzAS4uxZUKdASuFX0KJE43C6DaqL+y3VTpUFLI2FIkAS6HZ4I1YBi+S3hg==} + engines: {node: '>= 4'} + + ember-cli-version-checker@3.1.3: + resolution: {integrity: sha512-PZNSvpzwWgv68hcXxyjREpj3WWb81A7rtYNQq1lLEgrWIchF8ApKJjWP3NBpHjaatwILkZAV8klair5WFlXAKg==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-cli-version-checker@4.1.1: + resolution: {integrity: sha512-bzEWsTMXUGEJfxcAGWPe6kI7oHEGD3jaxUWDYPTqzqGhNkgPwXTBgoWs9zG1RaSMaOPFnloWuxRcoHi4TrYS3Q==} + engines: {node: 8.* || 10.* || >= 12.*} + + ember-cli-version-checker@5.1.2: + resolution: {integrity: sha512-rk7GY+FmLn/2e22HsZs0Ycrz8HQ1W3Fv+2TFOuEFW9optnDXDgkntPBIl6gact/LHsfBM5RKbM3dHsIIeLgl0Q==} + engines: {node: 10.* || >= 12.*} + + ember-cli@3.24.0: + resolution: {integrity: sha512-dLurYpluRcE+XjCHy/JzUBcW4dBKhjmXH3zUjyof89gFjj+8EFjB0b2tqyS6buKqBasinVaX8lZZVIXYCdFtNA==} + engines: {node: 10.* || >= 12} + hasBin: true + + ember-compatibility-helpers@1.2.7: + resolution: {integrity: sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA==} + engines: {node: 10.* || >= 12.*} + + ember-composable-helpers@5.0.0: + resolution: {integrity: sha512-gyUrjiSju4QwNrsCLbBpP0FL6VDFZaELNW7Kbcp60xXhjvNjncYgzm4zzYXhT+i1lLA6WEgRZ3lOGgyBORYD0w==} + engines: {node: 12.* || 14.* || >= 16} + + ember-concurrency-decorators@2.0.3: + resolution: {integrity: sha512-r6O34YKI/slyYapVsuOPnmaKC4AsmBSwvgcadbdy+jHNj+mnryXPkm+3hhhRnFdlsKUKdEuXvl43lhjhYRLhhA==} + engines: {node: 10.* || >= 12} + + ember-concurrency@1.3.0: + resolution: {integrity: sha512-DwGlfWFpYyAkTwsedlEtK4t1DznJSculAW6Vq5S1C0shVPc5b6tTpHB2FFYisannSYkm+wpm1f1Pd40qiNPtOQ==} + engines: {node: 8.* || >= 10.*} + + ember-concurrency@2.3.7: + resolution: {integrity: sha512-sz6sTIXN/CuLb5wdpauFa+rWXuvXXSnSHS4kuNzU5GSMDX1pLBWSuovoUk61FUe6CYRqBmT1/UushObwBGickQ==} + engines: {node: 10.* || 12.* || 14.* || >= 16} + + ember-cookies@0.5.2: + resolution: {integrity: sha512-nZ7oG97kBcO9UHjO95ryABpnVx62Bhxo7lIsBJNmWvFXleILm9DGueiAynzXxuYWWPuKIeeSbYakrS1869tNTw==} + engines: {node: 8.* || 10.* || >= 12.*} + + ember-could-get-used-to-this@1.0.1: + resolution: {integrity: sha512-+BXc8NbJJ/DoUXZk4e3qeRvADw9ThMEW7jm+Nz+QX9jRY/5QYodnFeUEfsC4Sw3Ti2m1+RzI2Lyq0QWpgOqFYg==} + engines: {node: 8.* || >= 10.*} + + ember-css-transitions@4.4.1: + resolution: {integrity: sha512-QSUc+bDNMU/3vbJZuSWz14a4V/wQbHDq/zY37pzqzZRMXBANGwpjy+KXTyWqDqi1XeXvnMf2BX/nbjYSC3ycbg==} + + ember-data@3.24.0: + resolution: {integrity: sha512-DdoJ573ucwXBBx3mR3eYwSCBChDGv3cDWaCu4x6lGmvHjropvoOVJmuXDhCuxtI17Zly7ATlXarfPZdmPA95QQ==} + engines: {node: 10.* || >= 12.*} + + ember-decorators@6.1.1: + resolution: {integrity: sha512-63vZPntPn1aqMyeNRLoYjJD+8A8obd+c2iZkJflswpDRNVIsp2m7aQdSCtPt4G0U/TEq2251g+N10maHX3rnJQ==} + engines: {node: '>= 8.*'} + + ember-destroyable-polyfill@2.0.3: + resolution: {integrity: sha512-TovtNqCumzyAiW0/OisSkkVK93xnVF4NRU6+FN0ubpfwEOpRrmM2RqDwXI6YAChCgSHON1cz0DfQStpA1Gjuuw==} + engines: {node: 10.* || >= 12} + + ember-drag-drop@0.4.8: + resolution: {integrity: sha512-wir1V/8yNz4qjd7LuQ5OxVOGwSCAoEaDYf427HalhHsHDdy+Q+z1U/rvJwefQs/p2fb3sN1A44T8Eb+AH3nv0Q==} + + ember-element-helper@0.2.0: + resolution: {integrity: sha512-/WV0PNLyxDvLX/YETb/8KICFTr719OYqFWXqV5XUkh9YhhBGDU/mr1OtlQaWOlsx+sHm42HD2UAICecqex8ziw==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-element-helper@0.6.1: + resolution: {integrity: sha512-YiOdAMlzYul4ulkIoNp8z7iHDfbT1fbut/9xGFRfxDwU/FmF8HtAUB2f1veu/w50HTeZNopa1OV2PCloZ76XlQ==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + ember-source: ^3.8 || 4 + + ember-ella-sparse@0.16.0: + resolution: {integrity: sha512-JLea/LYH3Juy0XzDCulmmpVXSOEiJIT6Pg4M3yXNVBQasU0eNj+ssO9Orpd7AHq5X4aFGWcKUnsVod3JFuMOfw==} + engines: {node: 8.* || >= 10.*} + + ember-eslint-parser@0.5.13: + resolution: {integrity: sha512-b6ALDaxs9Bb4v0uagWud/5lECb78qpXHFv7M340dUHFW4Y0RuhlsfA4Rb+765X1+6KHp8G7TaAs0UgggWUqD3g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@babel/core': ^7.23.6 + '@typescript-eslint/parser': '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + ember-exam@6.0.1: + resolution: {integrity: sha512-L63kB4Dxon7R/SAbGpvpJUlVFFlVbDDqfI7YYitXhufc2uKuhx3TkZfld9prEbaVPExKw4EuIWZkkuYjwJbttQ==} + engines: {node: 10.* || 12.* || >= 14} + peerDependencies: + ember-mocha: '*' + ember-qunit: '*' + peerDependenciesMeta: + ember-mocha: + optional: true + ember-qunit: + optional: true + + ember-export-application-global@2.0.1: + resolution: {integrity: sha512-B7wiurPgsxsSGzJuPFkpBWnaeuCu2PGpG2BjyrfA1VcL7//o+5RSnZqiCEY326y7qmxb2GoCgo0ft03KBU0rRw==} + engines: {node: '>= 4'} + + ember-factory-for-polyfill@1.3.1: + resolution: {integrity: sha512-y3iG2iCzH96lZMTWQw6LWNLAfOmDC4pXKbZP6FxG8lt7GGaNFkZjwsf+Z5GAe7kxfD7UG4lVkF7x37K82rySGA==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-fetch@8.1.2: + resolution: {integrity: sha512-TVx24/jrvDIuPL296DV0hBwp7BWLcSMf0I8464KGz01sPytAB+ZAePbc9ooBTJDkKZEGFgatJa4nj3yF1S9Bpw==} + engines: {node: '>= 10'} + + ember-get-config@0.5.0: + resolution: {integrity: sha512-y1osD6g8wV/BlDjuaN6OG5MT0iHY2X/yE38gUj/05uUIMIRfpcwOdWnFQHBiXIhDojvAJQTEF1VOYFIETQMkeQ==} + engines: {node: 12.* || 14.* || >= 16} + + ember-get-config@2.1.1: + resolution: {integrity: sha512-uNmv1cPG/4qsac8oIf5txJ2FZ8p88LEpG4P3dNcjsJS98Y8hd0GPMFwVqpnzI78Lz7VYRGQWY4jnE4qm5R3j4g==} + engines: {node: 12.* || 14.* || >= 16} + + ember-getowner-polyfill@2.2.0: + resolution: {integrity: sha512-rwGMJgbGzxIAiWYjdpAh04Abvt0s3HuS/VjHzUFhVyVg2pzAuz45B9AzOxYXzkp88vFC7FPaiA4kE8NxNk4A4Q==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-in-element-polyfill@1.0.1: + resolution: {integrity: sha512-eHs+7D7PuQr8a1DPqsJTsEyo3FZ1XuH6WEZaEBPDa9s0xLlwByCNKl8hi1EbXOgvgEZNHHi9Rh0vjxyfakrlgg==} + engines: {node: 10.* || >= 12} + + ember-in-viewport@4.1.0: + resolution: {integrity: sha512-3y6qWXuJPPc6vX2GfxWgtr+sDjb+bdZF9babstr0lTd8t8c1b42gJ13GaJqlylZIyZz2dEXFCimX9WAeudPv9g==} + engines: {node: 12.* || 14.* || >= 16} + + ember-infinity@2.3.0: + resolution: {integrity: sha512-WDTBtL04g7y3OaCHvRRaGl9c6ZKDpm1rpFVnlWWBC4GZY7kDDs4WxTbngdHjWZb9BRJ3u0ltLEe3ncYDioiSyA==} + engines: {node: 10.* || >= 12.*} + + ember-inflector@3.0.1: + resolution: {integrity: sha512-fngrwMsnhkBt51KZgwNwQYxgURwV4lxtoHdjxf7RueGZ5zM7frJLevhHw7pbQNGqXZ3N+MRkhfNOLkdDK9kFdA==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-inflector@4.0.3: + resolution: {integrity: sha512-E+NnmzybMRWn1JyEfDxY7arjOTJLIcGjcXnUxizgjD4TlvO1s3O65blZt+Xq2C2AFSPeqHLC6PXd6XHYM8BxdQ==} + engines: {node: 14.* || 16.* || >= 18} + peerDependencies: + ember-source: ^3.16.0 || ^4.0.0 || ^5.0.0 + + ember-keyboard@8.2.1: + resolution: {integrity: sha512-wT9xpt3GKsiodGZoifKU4OyeRjXWlmKV9ZHHsp6wJBwMFpl4wWPjTNdINxivk2qg/WFNIh8nUiwuG4+soWXPdw==} + peerDependencies: + '@ember/test-helpers': ^2.6.0 || ^3.0.0 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + + ember-load-initializers@2.1.2: + resolution: {integrity: sha512-CYR+U/wRxLbrfYN3dh+0Tb6mFaxJKfdyz+wNql6cqTrA0BBi9k6J3AaKXj273TqvEpyyXegQFFkZEiuZdYtgJw==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-load@0.0.17: + resolution: {integrity: sha512-gyJrGwUHOri9En8QG/v2NIwxHFC5DFifar+1TRLwsNSeyJaUSwoFExLCVN8xYoDrI5CX1lmRoOUcfcrEApKwfg==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-maybe-import-regenerator@0.1.6: + resolution: {integrity: sha512-aX9UINiUXIjzsCNNna1ioASB/2lbnFgSHI63bBcd4MOVE9AqoLdOL7h+ocyylYXyYoBj2JDRwCzjWNf2Xbp5wg==} + engines: {node: '>= 0.10.0'} + + ember-maybe-in-element@2.1.0: + resolution: {integrity: sha512-6WAzPbf4BNQIQzkur2+zRJJJ/PKQoujIYgFjrpj3fOPy8iRlxVUm0/B41qbFyg1LE6bVbg0cWbuESWEvJ9Rswg==} + engines: {node: 10.* || >= 12} + + ember-mocha@0.16.2: + resolution: {integrity: sha512-K3ZoOgMSGQJB9EwbYFij2v+fsNEzneOr2NLMGeI+6xaPpRYzcAS6Y5wBzsqvayI+7f5c1oyuca7jSg8vX+NE+w==} + engines: {node: 8.* || >= 10.*} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + ember-modifier-manager-polyfill@1.2.0: + resolution: {integrity: sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-modifier@3.2.7: + resolution: {integrity: sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==} + engines: {node: 12.* || >= 14} + + ember-modifier@4.2.0: + resolution: {integrity: sha512-BJ48eTEGxD8J7+lofwVmee7xDgNDgpr5dd6+MSu4gk+I6xb35099RMNorXY5hjjwMJEyi/IRR6Yn3M7iJMz8Zw==} + peerDependencies: + ember-source: ^3.24 || >=4.0 + peerDependenciesMeta: + ember-source: + optional: true + + ember-moment@10.0.1: + resolution: {integrity: sha512-Es5PaFBRF6djyb0zwmcCMqWjLhyKrCWs5Ug6iaZoKxDgEc8incVCTv3ED2Z4oCcNDY2gKTqXY5VTsP1J9hUPcg==} + peerDependencies: + moment: 2.24.0 + moment-timezone: 0.5.45 + peerDependenciesMeta: + moment: + optional: true + moment-timezone: + optional: true + + ember-one-way-select@4.0.1: + resolution: {integrity: sha512-B1Li794T1lmg7/d+PSQMnfPTCnDA+FBYD+UOpdIZ9oqMrX0NtQLeERzNVqOR7MbsDAvMkvQFV3YSIVQOuKJjhg==} + engines: {node: 10.* || >= 12} + + ember-power-calendar@0.15.0: + resolution: {integrity: sha512-530uXI1nVYs3GJGjqblQuY0s0/Ucp8w4aY3h+jX2ZEyuo/ym1xsEFB+r+OkWZ/LLzfWtRibmeG8ABvB0gXukvw==} + engines: {node: 10.* || >= 12} + + ember-power-datepicker@0.8.1: + resolution: {integrity: sha512-Y1+PYfCPooVzjRkBfZ/QQH0aE5hpV3rS2Cyj13mzoZ+pFuyQEigvI2C4H5//ckSk2Kc9Uj+d+0lzq7P+Ymf3Tw==} + engines: {node: 10.* || >= 12} + + ember-power-select@6.0.1: + resolution: {integrity: sha512-YslsjEUzdHhFfUP7IlklQuKt6rFG/VS38JLCjTYiCcBKrl76pxky/PoGMx3V+Ukh5mI77mGfA7BSKpKv8MAQAw==} + engines: {node: 14.* || >= 16} + + ember-raf-scheduler@0.3.0: + resolution: {integrity: sha512-i8JWQidNCX7n5TOTIKRDR0bnsQN9aJh/GtOJKINz2Wr+I7L7sYVhli6MFqMYNGKC9j9e6iWsznfAIxddheyEow==} + engines: {node: 12.* || 14.* || >= 16} + + ember-resolver@8.1.0: + resolution: {integrity: sha512-MGD7X2ztZVswGqs1mLgzhZJRhG7XiF6Mg4DgC7xJFWRYQQUHyGJpGdNWY9nXyrYnRIsCrQoL1do41zpxbrB/cg==} + engines: {node: '>= 10.*'} + + ember-rfc176-data@0.3.18: + resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} + + ember-router-generator@2.0.0: + resolution: {integrity: sha512-89oVHVJwmLDvGvAUWgS87KpBoRhy3aZ6U0Ql6HOmU4TrPkyaa8pM0W81wj9cIwjYprcQtN9EwzZMHnq46+oUyw==} + engines: {node: 8.* || 10.* || >= 12} + + ember-simple-auth@5.0.0: + resolution: {integrity: sha512-wGf2jTvOKo11jNmWYBIZhgMx32kvAJEMNJNCpbdx0Ak6X+4uzO8l203C5kDH0idZej/2dHgYkcVaEzl2HCVFwA==} + engines: {node: '>= 16'} + peerDependencies: + ember-fetch: ^8.0.1 + + ember-sinon@5.0.0: + resolution: {integrity: sha512-dTP2vhao1xWm3OlfpOALooso/OLM71SFg7PIBmZ6JdwKCC+CzcPb4BYRAXuoAFYzmhH8z28p8HdemjZBb0B3Bw==} + engines: {node: 10.* || >= 12} + + ember-source-channel-url@3.0.0: + resolution: {integrity: sha512-vF/8BraOc66ZxIDo3VuNP7iiDrnXEINclJgSJmqwAAEpg84Zb1DHPI22XTXSDA+E8fW5btPUxu65c3ZXi8AQFA==} + engines: {node: 10.* || 12.* || >= 14} + hasBin: true + + ember-source@3.24.0: + resolution: {integrity: sha512-tFNwicD33IQ9LRIc0tkS0xqKbz7w0P5x799DH4DjP+VEpHAn77AaBR5Cfs3oEFbVqMFFBl88M8V4zlI4Akwwhw==} + engines: {node: 10.* || >= 12.*} + + ember-style-modifier@1.0.0: + resolution: {integrity: sha512-ANkYpOeI3/tkRxVz750ymb8cQBqfBTKOUOz4RPRsNys8rlaBiaKEa95XLz1JWfCXCzjmqe8i2cIdnAMix+nb3A==} + engines: {node: 14.* || >= 16} + + ember-svg-jar@2.7.1: + resolution: {integrity: sha512-Y957CMG/IlpdrIu+Gl5KMYBZN6zL2Eyz2CQl2RRfpExFA3uJpv1+Br8vbtxLEs1OoPTlR+YYHcUXfgqS7dHaXw==} + engines: {node: 12.* || 14.* || >= 16} + + ember-template-imports@3.4.2: + resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} + engines: {node: 12.* || >= 14} + + ember-template-lint@5.13.0: + resolution: {integrity: sha512-AYxz9S9fVZfHPmTsymc7NwsD7FVmDUZyfC+KYpxDlK0wic7JSQx2FNQNqQSBFRLOuzn7VQ0/+1pX6DGqKDGswg==} + engines: {node: ^14.18.0 || ^16.0.0 || >= 18.0.0} + hasBin: true + + ember-template-recast@6.1.5: + resolution: {integrity: sha512-VnRN8FzEHQnw/5rCv6Wnq8MVYXbGQbFY+rEufvWV+FO/IsxMahGEud4MYWtTA2q8iG+qJFrDQefNvQ//7MI7Qw==} + engines: {node: 12.* || 14.* || >= 16.*} + hasBin: true + + ember-test-selectors@6.0.0: + resolution: {integrity: sha512-PgYcI9PeNvtKaF0QncxfbS68olMYM1idwuI8v/WxsjOGqUx5bmsu6V17vy/d9hX4mwmjgsBhEghrVasGSuaIgw==} + engines: {node: 12.* || 14.* || >= 16.*} + + ember-test-waiters@1.2.0: + resolution: {integrity: sha512-aEw7YuutLuJT4NUuPTNiGFwgTYl23ThqmBxSkfFimQAn+keWjAftykk3dlFELuhsJhYW/S8YoVjN0bSAQRLNtw==} + engines: {node: 6.* || 8.* || >= 10.*} + + ember-text-measurer@0.6.0: + resolution: {integrity: sha512-/aZs2x2i6kT4a5tAW+zenH2wg8AbRK9jKxLkbVsKl/1ublNl27idVRdov1gJ+zgWu3DNK7whcfVycXtlaybYQw==} + engines: {node: 10.* || >= 12} + + ember-tooltips@3.6.0: + resolution: {integrity: sha512-DsqF6vvL3DKWSUHJKuMJ8KSxbE/T+eZAE2xtzAuHRqjl1AYIOkugGBVynGYYP8+2/10NMwk05LYbT1dirAcEBQ==} + engines: {node: 12.* || 14.* || >= 16} + + ember-tracked-storage-polyfill@1.0.0: + resolution: {integrity: sha512-eL7lZat68E6P/D7b9UoTB5bB5Oh/0aju0Z7PCMi3aTwhaydRaxloE7TGrTRYU+NdJuyNVZXeGyxFxn2frvd3TA==} + engines: {node: 12.* || >= 14} + + ember-truth-helpers@2.1.0: + resolution: {integrity: sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + ember-truth-helpers@3.1.1: + resolution: {integrity: sha512-FHwJAx77aA5q27EhdaaiBFuy9No+8yaWNT5A7zs0sIFCmf14GbcLn69vJEp6mW7vkITezizGAWhw7gL0Wbk7DA==} + engines: {node: 10.* || >= 12} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.6: + resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@4.5.0: + resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} + engines: {node: '>=6.9.0'} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + ensure-posix-path@1.1.1: + resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + eol@0.9.1: + resolution: {integrity: sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error@7.2.1: + resolution: {integrity: sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-iterator-helpers@1.3.1: + resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.2: + resolution: {integrity: sha512-cQpUid7bdTUnFin8S7BnNdOk+/eDqQmKgCANSyd/jAhrKEvxUvr9VQ8XZzXiOtest8NLfk3FSBZzwvemZNQ6Vg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-formatter-kakoune@1.0.0: + resolution: {integrity: sha512-Uk/TVLt6Nf6Xoz7C1iYuZjOSdJxe5aaauGRke8JhKeJwD66Y61/pY2FjtLP04Ooq9PwV34bzrkKkU2UZ5FtDRA==} + + eslint-plugin-babel@5.3.1: + resolution: {integrity: sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==} + engines: {node: '>=4'} + peerDependencies: + eslint: '>=4.0.0' + + eslint-plugin-ember@12.7.5: + resolution: {integrity: sha512-2zLEpu3xcKjykgsKkj8sU2GwdxADFTH5XPBvuIrNBP253JxHSz2P21isUuRB50kGoR2KL+eUHNgV0j7IPCav1w==} + engines: {node: 18.* || 20.* || >= 21} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '>= 8' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-filenames-ts@1.3.2: + resolution: {integrity: sha512-ZgcMHTYL4JXBmvbUYAyNeqKIwC038v6Jru+FxJlZ2vk3XOaWIiFphcjbVS/7bdIRADMnXYI8xfgY9g/xtIoDVw==} + peerDependencies: + eslint: '*' + + eslint-plugin-ghost@3.5.0: + resolution: {integrity: sha512-5LMpq8rZIDz66M6FMsDWGPfrVUcItWD06+HYiR9dXUrnl/9qw3zffNkLM0GJDb8mJVELLlSOs+2d1NwTROaKqg==} + peerDependencies: + eslint: '>=5.11.0' + + eslint-plugin-i18next@6.1.3: + resolution: {integrity: sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw==} + engines: {node: '>=18.10.0'} + + eslint-plugin-mocha@7.0.1: + resolution: {integrity: sha512-zkQRW9UigRaayGm/pK9TD5RjccKXSgQksNtpsXbG9b6L5I+jNx7m98VUbZ4w1H1ArlNA+K7IOH+z8TscN6sOYg==} + engines: {node: '>=10.0.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-n@17.24.0: + resolution: {integrity: sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=8.23.0' + + eslint-plugin-no-relative-import-paths@1.6.1: + resolution: {integrity: sha512-YZNeOnsOrJcwhFw0X29MXjIzu2P/f5X2BZDPWw1R3VUYBRFxNIh77lyoL/XrMU9ewZNQPcEvAgL/cBOT1P330A==} + + eslint-plugin-playwright@2.10.1: + resolution: {integrity: sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==} + engines: {node: '>=16.9.0'} + peerDependencies: + eslint: '>=8.40.0' + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.24: + resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-sort-imports-es6-autofix@0.6.0: + resolution: {integrity: sha512-2NVaBGF9NN+727Fyq+jJYihdIeegjXeUUrZED9Q8FVB8MsV3YQEyXG96GVnXqWt0pmn7xfCZOZf3uKnIhBrfeQ==} + peerDependencies: + eslint: '>=7.7.0' + + eslint-plugin-storybook@10.3.3: + resolution: {integrity: sha512-jo8wZvKaJlxxrNvf4hCsROJP3CdlpaLiYewAs5Ww+PJxCrLelIi5XVHWOAgBvvr3H9WDKvUw8xuvqPYqAlpkFg==} + peerDependencies: + eslint: '>=8' + storybook: ^10.3.3 + + eslint-plugin-tailwindcss@3.18.2: + resolution: {integrity: sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA==} + engines: {node: '>=18.12.0'} + peerDependencies: + tailwindcss: ^3.4.0 + + eslint-plugin-tailwindcss@4.0.0-beta.0: + resolution: {integrity: sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA==} + engines: {node: '>=18.12.0'} + peerDependencies: + tailwindcss: ^3.4.0 || ^4.0.0 + + eslint-plugin-unicorn@42.0.0: + resolution: {integrity: sha512-ixBsbhgWuxVaNlPTT8AyfJMlhyC5flCJFjyK3oKE8TRrwBnaHvUbuIkCM1lqg8ryYrFStL/T557zfKzX4GKSlg==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=8.8.0' + + eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + + eslint-scope@4.0.3: + resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==} + engines: {node: '>=4.0.0'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-utils@3.0.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@9.37.0: + resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@3.0.0: + resolution: {integrity: sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw==} + engines: {node: '>=0.10.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events-to-array@1.1.2: + resolution: {integrity: sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + + exec-sh@0.3.6: + resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@2.1.0: + resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} + engines: {node: ^8.12.0 || >=9.7.0} + + execa@3.4.0: + resolution: {integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==} + engines: {node: ^8.12.0 || >=9.7.0} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + exists-sync@0.0.4: + resolution: {integrity: sha512-cy5z7K+05RFxHAWY37dSDkPWmuTi+VzrA/xLwPDHmwQPMnO/kVhu6jheGaItlnNRoOE6f5MAjxy3VEupfrHigQ==} + deprecated: Please replace with usage of fs.existsSync + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + expect@28.1.3: + resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-brute@1.0.1: + resolution: {integrity: sha512-ieZmwox3oIZdQCVjvvnwQvrKQumWdb/JjmC9mWplF42AuHCBXr6Yk/I+nLTRQx+9F+2aapOW9kYLwA6xIlwA9g==} + peerDependencies: + express: 4.x + + express-end@0.0.8: + resolution: {integrity: sha512-PPntzICAq006LBpXKBVJtmRUiCRqTMZ+OB8L2RFXgx+OmkMWU66IL4DTEPF/DOcxmsuC7Y0NdbT2R71lb+pBpg==} + engines: {node: '>=0.10'} + + express-handlebars@7.1.3: + resolution: {integrity: sha512-O0W4n14iQ8+iFIDdiMh9HRI2nbVQJ/h1qndlD1TXWxxcfbKjKoqJh+ti2tROkyx4C4VQrt0y3bANBQ5auQAiew==} + engines: {node: '>=v16'} + + express-hbs@2.5.0: + resolution: {integrity: sha512-i2O1ZBwKO32KF0MePnkgYHsAAILr9H9Sp5GoGp9JWz/qhsBfTMSq9VF1pN109DHysPX6YO88y7B+f6xnEEF/mg==} + + express-jwt@8.5.1: + resolution: {integrity: sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==} + engines: {node: '>= 8.0.0'} + + express-lazy-router@1.0.6: + resolution: {integrity: sha512-YIyQku3r1uISL9oSVazQ8F16V6Hm6DJuPNISjPpgQ69u8bUUK/CYz4UpN/oWWd2lY6zEppF66r5ADagWFYJpng==} + peerDependencies: + express: ^4.0.0 + + express-query-boolean@2.0.0: + resolution: {integrity: sha512-4dU/1HPm8lkTPR12+HFUXqCarcsC19OKOkb4otLOuADfPYrQMaugPJkSmxNsqwmWYjozvT6vdTiqkgeBHkzOow==} + + express-queue@0.0.13: + resolution: {integrity: sha512-C4OEDasGDqpXLrZICSUxbY47p5c0bKqf/3/3hwauSCmI+jVVxKBWU2w39BuKLP6nF65z87uDFBbJMPAn2ZrG3g==} + engines: {node: '>=6'} + + express-session@1.19.0: + resolution: {integrity: sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==} + engines: {node: '>= 0.8.0'} + + express-unless@2.1.3: + resolution: {integrity: sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + extract-stack@2.0.0: + resolution: {integrity: sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==} + engines: {node: '>=8'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fake-xml-http-request@2.1.2: + resolution: {integrity: sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==} + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-ordered-set@1.0.3: + resolution: {integrity: sha512-MxBW4URybFszOx1YlACEoK52P6lE3xiFcPaGCUZ7QQOZ6uJXKo++Se8wa31SjcZ+NC/fdAWX7UtKEfaGgHS2Vg==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-sourcemap-concat@1.4.0: + resolution: {integrity: sha512-x90Wlx/2C83lfyg7h4oguTZN4MyaVfaiUSJQNpU+YEA0Odf9u659Opo44b0LfoVg9G/bOE++GdID/dkyja+XcA==} + engines: {node: '>= 4'} + + fast-sourcemap-concat@2.1.1: + resolution: {integrity: sha512-7h9/x25c6AQwdU3mA8MZDUMR3UCy50f237egBrBkuwjnUZSmfu4ptCf91PZSKzON2Uh5VvIHozYKWcPPgcjxIw==} + engines: {node: 10.* || >= 12.*} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + hasBin: true + + fastboot-transform@0.1.3: + resolution: {integrity: sha512-6otygPIJw1ARp1jJb+6KVO56iKBjhO+5x59RSC9qiZTbZRrv+HZAuP00KD3s+nWMvcFDemtdkugki9DNFTTwCQ==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + figgy-pudding@3.5.2: + resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} + deprecated: This module is no longer supported. + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-entry-cache@7.0.2: + resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==} + engines: {node: '>=12.0.0'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-extension@4.0.5: + resolution: {integrity: sha512-l0rOL3aKkoi6ea7MNZe6OHgqYYpn48Qfflr8Pe9G9JPPTx5A+sfboK91ZufzIs59/lPqh351l0eb6iKU9J5oGg==} + engines: {node: '>=4'} + + file-selector@0.6.0: + resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} + engines: {node: '>= 12'} + + file-system-cache@2.3.0: + resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} + + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filesize@4.2.1: + resolution: {integrity: sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==} + engines: {node: '>= 0.4.0'} + + filesize@6.4.0: + resolution: {integrity: sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==} + engines: {node: '>= 0.4.0'} + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-babel-config@1.2.2: + resolution: {integrity: sha512-oK59njMyw2y3yxto1BCfVK7MQp/OYf4FleHu0RgosH3riFJ1aOuo/7naLDLAObfrgn3ueFhw5sAT/cp0QuJI3Q==} + engines: {node: '>=4.0.0'} + + find-babel-config@2.1.2: + resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + + find-index@1.1.1: + resolution: {integrity: sha512-XYKutXMrIK99YMUPf91KX5QVJoG31/OsgftD6YoTPAObfQIxM4ziA9f0J1AsqKhJmo+IeaIPP0CFopTD4bdUBw==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + find-yarn-workspace-root@1.2.1: + resolution: {integrity: sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==} + + find-yarn-workspace-root@2.0.0: + resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + + findup-sync@3.0.0: + resolution: {integrity: sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==} + engines: {node: '>= 0.10'} + + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + + fined@1.2.0: + resolution: {integrity: sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==} + engines: {node: '>= 0.10'} + + fireworm@0.7.2: + resolution: {integrity: sha512-GjebTzq+NKKhfmDxjKq3RXwQcN9xRmZWhnnuC9L+/x5wBQtR0aaQM50HsjrzJ2wc28v1vSdfOpELok0TKR4ddg==} + + fixturify-project@1.10.0: + resolution: {integrity: sha512-L1k9uiBQuN0Yr8tA9Noy2VSQ0dfg0B8qMdvT7Wb5WQKc7f3dn3bzCbSrqlb+etLW+KDV4cBC7R1OvcMg3kcxmA==} + + fixturify-project@2.1.1: + resolution: {integrity: sha512-sP0gGMTr4iQ8Kdq5Ez0CVJOZOGWqzP5dv/veOTdFNywioKjkNWCHBi1q65DMpcNGUGeoOUWehyji274Q2wRgxA==} + engines: {node: 10.* || >= 12.*} + + fixturify@1.3.0: + resolution: {integrity: sha512-tL0svlOy56pIMMUQ4bU1xRe6NZbFSa/ABTWMxW2mH38lFGc9TrNAKWcMBQ7eIjo3wqSS8f2ICabFaatFyFmrVQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + fixturify@2.1.1: + resolution: {integrity: sha512-SRgwIMXlxkb6AUgaVjIX+jCEqdhyXu9hah7mcK+lWynjKtX73Ux1TDv71B7XyaQ+LJxkYRHl5yCL8IycAvQRUw==} + engines: {node: 10.* || >= 12.*} + + flagged-respawn@1.0.1: + resolution: {integrity: sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==} + engines: {node: '>= 0.10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + flatten@1.0.3: + resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==} + deprecated: flatten is deprecated in favor of utility frameworks such as lodash. + + flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + + flexsearch@0.8.153: + resolution: {integrity: sha512-IwD3iA4OcN+H5XihfbWEhK/QShx/31WT+RAB2ni/ICguQX+1w3iyP1w8mYEnCkQvj4jbAAE/uujaZocoB8TLKg==} + + flush-write-stream@1.1.1: + resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} + + focus-trap@6.9.4: + resolution: {integrity: sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + for-own@1.0.0: + resolution: {integrity: sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==} + engines: {node: '>=0.10.0'} + + foreach@2.0.6: + resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + form-data-encoder@4.1.0: + resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} + engines: {node: '>= 18'} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + form-data@3.0.4: + resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} + engines: {node: '>= 6'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@1.2.6: + resolution: {integrity: sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==} + deprecated: 'Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau' + + formidable@2.1.5: + resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@0.24.0: + resolution: {integrity: sha512-w1RvhdLZdU9V3vQdL+RooGlo6b9R9WVoBanOfoJvosWlqSKvrjFlci2oVhwvLwZXBtM7khyPvZ8r3fwsim3o0A==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fs-extra@4.0.3: + resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + + fs-extra@5.0.0: + resolution: {integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==} + + fs-extra@6.0.1: + resolution: {integrity: sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-merger@3.2.1: + resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs-mkdirp-stream@2.0.1: + resolution: {integrity: sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==} + engines: {node: '>=10.13.0'} + + fs-tree-diff@0.5.9: + resolution: {integrity: sha512-872G8ax0kHh01m9n/2KDzgYwouKza0Ad9iFltBpNykvROvf2AGtoOzPJgGx125aolGPER3JuC7uZFrQ7bG1AZw==} + + fs-tree-diff@2.0.1: + resolution: {integrity: sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==} + engines: {node: 6.* || 8.* || >= 10.*} + + fs-updater@1.0.4: + resolution: {integrity: sha512-0pJX4mJF/qLsNEwTct8CdnnRdagfb+LmjRPJ8sO+nCnAZLW0cTmz4rTgU25n+RvTuWSITiLKrGVJceJPBIPlKg==} + engines: {node: '>=6.0.0'} + + fs-write-stream-atomic@1.0.10: + resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} + deprecated: This package is no longer supported. + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: Upgrade to fsevents v2 to mitigate potential security issues + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuse.js@6.6.2: + resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==} + engines: {node: '>=10'} + + fwd-stream@1.0.4: + resolution: {integrity: sha512-q2qaK2B38W07wfPSQDKMiKOD5Nzv2XyuvQlrmh1q0pxyHNanKHq8lwQ6n9zHucAwA5EbzRJKEgds2orn88rYTg==} + + gauge@2.7.4: + resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==} + deprecated: This package is no longer supported. + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + gelf-stream@1.1.1: + resolution: {integrity: sha512-kCzCfI6DJ8+aaDhwMcsNm2l6CsBj6y4Is6CCxH2W9sYnZGcXg9WmJ/iZMoJVO6uTwTRL7dbIioAS8lCuGUXSFA==} + + gelfling@0.3.1: + resolution: {integrity: sha512-vli3D2RYpLW6XhryNrv7HMjFNbj+ks/CCVDjokxOtZ+p6QYRadj8Zc0ps+LolSsh/I97XO0OduP/ShOej08clA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stdin@4.0.1: + resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} + engines: {node: '>=0.10.0'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + getopts@2.2.5: + resolution: {integrity: sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + ghost-storage-base@1.1.2: + resolution: {integrity: sha512-5NG3cm7dbb5RPFcviNlXJuwIN5ydDEaMEY5+ixrMAYspRLLEtAAtUYF+YDkBfpJc86aopqMshcszvwDzE7QCnA==} + + git-hooks-list@1.0.3: + resolution: {integrity: sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==} + + git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@3.1.0: + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-stream@8.0.3: + resolution: {integrity: sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@3.2.11: + resolution: {integrity: sha512-hVb0zwEZwC1FXSKRPFTeOtN7AArJcJlI6ULGLtrstaswKNlrTJqAA+1lYlSUop4vjA423xlBzqfVS3iWGlqJ+g==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@5.0.15: + resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@6.0.4: + resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + + globals@9.18.0: + resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} + engines: {node: '>=0.10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globby@10.0.0: + resolution: {integrity: sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + + google-caja-bower@https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd: + resolution: {tarball: https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd} + version: 6011.0.0 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + got@13.0.0: + resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} + engines: {node: '>=16'} + + got@14.6.6: + resolution: {integrity: sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==} + engines: {node: '>=20'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + growl@1.9.2: + resolution: {integrity: sha512-RTBwDHhNuOx4F0hqzItc/siXCasGfC4DeWcBamclWd+6jWtBaeB/SGbMkGf0eiQoW7ib8JpvOgnUsmgMHI3Mfw==} + + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + + gscan@5.4.3: + resolution: {integrity: sha512-KFxq96Sr1bVy+ExNWIXLgKgkYitb3xnF+hqdZsHX/rMY53PIM9zV3rPf4onN0YbDl2a33vq2cWGn8dx6qxzodw==} + engines: {node: ^20.19.0 || ^22.13.1 || ^24.0.0} + hasBin: true + + gulp-sort@2.0.0: + resolution: {integrity: sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-ansi@3.0.0: + resolution: {integrity: sha512-5JRDTvNq6mVkaMHQVXrGnaCXHD6JfqxwCy8LA/DQSqLLqePR9uaJVm2u3Ek/UziJFQz+d1ul99RtfIhE2aorkQ==} + engines: {node: '>=4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + has-values@2.0.1: + resolution: {integrity: sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w==} + engines: {node: '>=6'} + + has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + hash-base@3.0.5: + resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} + engines: {node: '>= 0.10'} + + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + + hash-for-dep@1.5.2: + resolution: {integrity: sha512-+kJRJpgO+V8x6c3UQuzO+gzHu5euS8HDOIaIUsOPdQrVu7ajNKkMykbSC8O0VX3LuRnUNf4hHE0o/rJ+nB8czw==} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + heic-convert@2.1.0: + resolution: {integrity: sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==} + engines: {node: '>=12.0.0'} + + heic-decode@2.1.0: + resolution: {integrity: sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==} + engines: {node: '>=8.0.0'} + + heimdalljs-fs-monitor@1.1.2: + resolution: {integrity: sha512-M7OPf3Tu+ybhAXdiC07O1vUYFyhCgfew4L3vaG2nn4Be05xzNvtBcU6IKMTfHJ9AxWFa3w9rrmiJovkxHhpopw==} + + heimdalljs-graph@1.0.0: + resolution: {integrity: sha512-v2AsTERBss0ukm/Qv4BmXrkwsT5x6M1V5Om6E8NcDQ/ruGkERsfsuLi5T8jx8qWzKMGYlwzAd7c/idymxRaPzA==} + engines: {node: 8.* || >= 10.*} + + heimdalljs-logger@0.1.10: + resolution: {integrity: sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==} + + heimdalljs@0.2.6: + resolution: {integrity: sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==} + + heimdalljs@0.3.3: + resolution: {integrity: sha512-xRlqDhgaXW4WccsiQlv6avDMKVN9Jk+FyMopDRPkmdf92TqfGSd2Osd/PKrK9sbM1AKcj8OpPlCzNlCWaLagCw==} + + hex-color-regex@1.1.0: + resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + home-or-tmp@2.0.0: + resolution: {integrity: sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==} + engines: {node: '>=0.10.0'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + + hsl-regex@1.0.0: + resolution: {integrity: sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==} + + hsla-regex@1.0.0: + resolution: {integrity: sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==} + + html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-to-text@5.1.1: + resolution: {integrity: sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA==} + engines: {node: '>= 4.0.0'} + hasBin: true + + html-to-text@8.2.1: + resolution: {integrity: sha512-aN/3JvAk8qFsWVeE9InWAWueLXrbkoVZy0TkzaGhoRBC2gCFEeRLDDJN3/ijIGHohy6H+SZzUQWN/hcYtaPK8w==} + engines: {node: '>=10.23.2'} + hasBin: true + + html-validate@8.29.0: + resolution: {integrity: sha512-RFfFIWaUB9SN8iETL2GoPvjWEX1gcbz0+m+vao7xkPl0cnlgMDu9RcjdQz6T3n6LgT/LENPkvxHzVkqY/Qs3Tg==} + engines: {node: '>= 16.14'} + hasBin: true + peerDependencies: + jest: ^27.1 || ^28.1.3 || ^29.0.3 + jest-diff: ^27.1 || ^28.1.3 || ^29.0.3 + jest-snapshot: ^27.1 || ^28.1.3 || ^29.0.3 + vitest: ^0.34.0 || ^1.0.0 || ^2.0.0 + peerDependenciesMeta: + jest: + optional: true + jest-diff: + optional: true + jest-snapshot: + optional: true + vitest: + optional: true + + html2canvas-objectfit-fix@1.2.0: + resolution: {integrity: sha512-eBQPJyJJPgAlgtMioB42Kv+wHXGgfjUyPdYWZKbF8jUlss4J6xdj5tJM8n2uZ/94NU2UEooN97ypGdzbFE562A==} + engines: {node: '>=8.0.0'} + + html5parser@2.0.2: + resolution: {integrity: sha512-L0y+IdTVxHsovmye8MBtFgBvWZnq1C9WnI/SmJszxoQjmUH1psX2uzDk21O5k5et6udxdGjwxkbmT9eVRoG05w==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + htmlparser2@3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + + htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@1.4.0: + resolution: {integrity: sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw==} + engines: {node: '>= 0.6'} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + httpntlm@1.6.1: + resolution: {integrity: sha512-Tcz3Ct9efvNqw3QdTl3h6IgRRlIQxwKkJELN/aAIGnzi2xvb3pDHdnMs8BrxWLV6OoT4DlVyhzSVhFt/tk0lIw==} + engines: {node: '>=0.8.0'} + + httpreq@1.1.1: + resolution: {integrity: sha512-uhSZLPPD2VXXOSN8Cni3kIsoFHaU2pT/nySEU/fHr/ePbqHYr0jeiQRmUKLEirC09SFPsdMoA7LU7UXMd/w0Kw==} + engines: {node: '>= 6.15.1'} + + https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + https@1.0.0: + resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==} + + human-interval@2.0.1: + resolution: {integrity: sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==} + + human-number@2.0.10: + resolution: {integrity: sha512-RHuagJOf+jR/6FkLzz8ZugPUR4KGbqJOWdsYUOqa7j/M542M0gVe4a+TJNVQgqaAMTKX9wyvv/xuiAII0Kq6Iw==} + engines: {node: '>= 8'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18n-iso-countries@7.14.0: + resolution: {integrity: sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==} + engines: {node: '>= 12'} + + i18next-parser@8.13.0: + resolution: {integrity: sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==} + engines: {node: '>=16.0.0 || >=18.0.0 || >=20.0.0', npm: '>=6', yarn: '>=1'} + deprecated: Project is deprecated, use i18next-cli instead + hasBin: true + + i18next@23.16.8: + resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + idb-wrapper@1.7.2: + resolution: {integrity: sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + iferr@0.1.5: + resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-extensions@1.1.0: + resolution: {integrity: sha512-P0t7ByhK8Jk9TU05ct/7+f7h8dNuXq5OY4m0IO/T+1aga/qHkpC0Wf472x3FLdq/zFDG17pgapCM3JDTxwZzow==} + engines: {node: '>=0.10.0'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-fresh@2.0.0: + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + include-path-searcher@0.1.0: + resolution: {integrity: sha512-KlpXnsZOrBGo4PPKqPFi3Ft6dcRyh8fTaqgzqDRi8jKAsngJEWWOxeFIWC8EfZtXKaZqlsNf9XRwcQ49DVgl/g==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + indexes-of@1.0.1: + resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} + + indexof@0.0.1: + resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflected@2.1.0: + resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} + + inflection@1.12.0: + resolution: {integrity: sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==} + engines: {'0': node >= 0.4.0} + + inflection@1.13.4: + resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} + engines: {'0': node >= 0.4.0} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.1: + resolution: {integrity: sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==} + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + inline-source-map-comment@1.0.5: + resolution: {integrity: sha512-a3/m6XgooVCXkZCduOb7pkuvUtNKt4DaqaggKKJrMQHQsqt6JcJXEreExeZiiK4vWL/cM/uF6+chH05pz2/TdQ==} + hasBin: true + + inquirer@6.5.2: + resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==} + engines: {node: '>=6.0.0'} + + inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + + inquirer@8.2.7: + resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} + engines: {node: '>=12.0.0'} + + install-artifact-from-github@1.4.0: + resolution: {integrity: sha512-+y6WywKZREw5rq7U2jvr2nmZpT7cbWbQQ0N/qfcseYnzHFz2cZz1Et52oY+XttYuYeTkI8Y+R2JNWj68MpQFSg==} + hasBin: true + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + intersection-observer-admin@0.3.4: + resolution: {integrity: sha512-L9W5vBSu7W5Dx/RcbWcErNwoke3xWIAD2SADbmAZnsF9XL667keuKgaqnuwTY4XU9UwhzXref5tnNF5ZLTIQ3w==} + + intl-format-cache@4.3.1: + resolution: {integrity: sha512-OEUYNA7D06agqPOYhbTkl0T8HA3QKSuwWh1HiClEnpd9vw7N+3XsQt5iZ0GUEchp5CW1fQk/tary+NsbF3yQ1Q==} + + intl-messageformat-parser@2.1.3: + resolution: {integrity: sha512-YOP2GuQ8Y+f3j2Vti1/abcozYlkv/WsIffsRP8cgtN4Oha5hjhNnucxzyfmeAoDgDHw4s/lNyLylB01369a5lg==} + deprecated: We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser + + intl-messageformat@5.4.3: + resolution: {integrity: sha512-wiGIeeokY6D/TFC6JYoBIUmZJ2AzkTHJuWp5QulDH4h77aghFTBm+8MvjTLIUiQ/z7xiY3SUQN2UBG7EtwYTAg==} + + intl@1.2.5: + resolution: {integrity: sha512-rK0KcPHeBFBcqsErKSpvZnrOmWOj+EmDkyJ57e90YWaQNqbcivcqmKDlHEeNprDWOsKzPsh1BfSpPQdDvclHVw==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ioredis@4.31.0: + resolution: {integrity: sha512-tVrCrc4LWJwX82GD79dZ0teZQGq+5KJEGpXJRgzHOrhHtLgF9ME6rTwDV5+HN5bjnvmtrnS8ioXhflY16sy2HQ==} + engines: {node: '>=6'} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-absolute-url@2.1.0: + resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} + engines: {node: '>=0.10.0'} + + is-absolute-url@3.0.3: + resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==} + engines: {node: '>=8'} + + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumeric@1.0.0: + resolution: {integrity: sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==} + engines: {node: '>=0.10.0'} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-color-stop@1.1.0: + resolution: {integrity: sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-directory@0.3.1: + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@1.0.0: + resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-git-url@1.0.0: + resolution: {integrity: sha512-UCFta9F9rWFSavp9H3zHEHrARUfZbdJvmHKeEpds4BK3v7W2LdXoNypMtXXi5w5YBDEBCTYmbI+vsSwI8LYJaQ==} + engines: {node: '>=0.8'} + + is-glob@2.0.1: + resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==} + engines: {node: '>=0.10.0'} + + is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-invalid-path@0.1.0: + resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==} + engines: {node: '>=0.10.0'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-language-code@2.0.0: + resolution: {integrity: sha512-6xKmRRcP2YdmMBZMVS3uiJRPQgcMYolkD6hFw2Y4KjqyIyaJlCGxUt56tuu0iIV8q9r8kMEo0Gjd/GFwKrgjbw==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negated-glob@1.0.0: + resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} + engines: {node: '>=0.10.0'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-object@0.1.2: + resolution: {integrity: sha512-GkfZZlIZtpkFrqyAXPQSRBMsaHAw+CgoKe2HXAkjd/sfoI9+hS8PT4wg2rJxdQyUKr7N2vHJbg7/jQtE5l5vBQ==} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-relative-url@3.0.0: + resolution: {integrity: sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA==} + engines: {node: '>=8'} + + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + + is-resolvable@1.1.0: + resolution: {integrity: sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-string-and-not-blank@0.0.2: + resolution: {integrity: sha512-FyPGAbNVyZpTeDCTXnzuwbu9/WpNXbCfbHXLpCRpN4GANhS00eEIP5Ef+k5HYSNIzIhdN9zRDoBj6unscECvtQ==} + engines: {node: '>=6.4.0'} + + is-string-blank@1.0.1: + resolution: {integrity: sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-type@0.0.1: + resolution: {integrity: sha512-YwJh/zBVrcJ90aAnPBM0CbHvm7lG9ao7lIFeqTZ1UQj4iFLpM5CikdaU+dGGesrMJwxLqPGmjjrUrQ6Kn3Zh+w==} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-uri@1.2.13: + resolution: {integrity: sha512-6KXiOBu0Y9bvxaWzVWhDfXBdiE5jQWS5MIuRITXHdLVJ11LaOGYRYJtJqUQkjNyUbtCfZZyREdnEFlJfhzIlog==} + engines: {node: '>= 4'} + + is-url-superb@4.0.0: + resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} + engines: {node: '>=10'} + + is-valid-glob@1.0.0: + resolution: {integrity: sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==} + engines: {node: '>=0.10.0'} + + is-valid-path@0.1.1: + resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==} + engines: {node: '>=0.10.0'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-whitespace-character@1.0.4: + resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-word-character@1.0.4: + resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + is@0.2.7: + resolution: {integrity: sha512-ajQCouIvkcSnl2iRdK70Jug9mohIHVX9uKpoWnl115ov0R5mzBvRrXxrnHbsA+8AdwCwc/sfw7HXmd4I5EJBdQ==} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbuffer@0.0.0: + resolution: {integrity: sha512-xU+NoHp+YtKQkaM2HsQchYn0sltxMxew0HavMfHbjnucBoTSGbw745tL+Z7QBANleWM1eEQMenEpi174mIeS4g==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + iso-639-3@2.2.0: + resolution: {integrity: sha512-v9w/U4XDSfXCrXxf4E6ertGC/lTRX8MLLv7XC1j6N5oL3ympe38jp77zgeyMsn3MbufuAAoGeVzDJbOXnPTMhQ==} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isostring@0.0.1: + resolution: {integrity: sha512-wRcdJtXCe2LGtXnD14fXMkduWVdbeGkzBIKg8WcKeEOi6SIc+hRjYYw76WNx3v5FebhUWZrBTWB0NOl3/sagdQ==} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + istextorbinary@2.1.0: + resolution: {integrity: sha512-kT1g2zxZ5Tdabtpp9VSdOzW9lb6LXImyWbzbQeTxoRtHhurC9Ej9Wckngr2+uepPL09ky/mJHmN9jeJPML5t6A==} + engines: {node: '>=0.12'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jade@0.26.3: + resolution: {integrity: sha512-mkk3vzUHFjzKjpCXeu+IjXeZD+QOTjUUdubgmHtHTDwvAO2ZTkMTTVrapts5CWz3JvJryh/4KWZpjeZrCepZ3A==} + deprecated: Jade has been renamed to pug, please install the latest version of pug instead of jade + hasBin: true + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@28.1.3: + resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-extended@7.0.0: + resolution: {integrity: sha512-96jBsVJDxZKFh+kWY7E18Is2usUsUYtBn97MxCtb4COnbgD4aE1h+P0fdFQNeJaI6KOeduas4Numc9yTuk0+Gw==} + engines: {node: ^20.9.0 || ^22.11.0 || ^24.11.0 || >=25.0.0} + peerDependencies: + jest: '>=27.2.5' + typescript: '>=5.0.0' + peerDependenciesMeta: + jest: + optional: true + + jest-get-type@28.0.2: + resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@28.1.3: + resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@28.1.3: + resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@28.1.3: + resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@28.0.2: + resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@28.1.3: + resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@28.1.3: + resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@28.1.3: + resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + jquery-deferred@0.3.1: + resolution: {integrity: sha512-YTzoTYR/yrjmNh6B6exK7lC1jlDazEzt9ZlZvdRscv+I1AJqN1SmU3ZAn4iMGiVhwAavCrbijDVyTc0lmr9ZCA==} + engines: {node: '>=0.4.0'} + + jquery@3.7.1: + resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@3.0.2: + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsdoc-type-pratt-parser@4.8.0: + resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} + engines: {node: '>=12.0.0'} + + jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@29.0.1: + resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@0.3.0: + resolution: {integrity: sha512-UHQmAeTXV+iwEk0aHheJRqo6Or90eDxI6KIYpHSjKLXKuKlPt1CQ7tGBerFcFA8uKU5mYxiPMlckmFptd5XZzA==} + hasBin: true + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@1.3.0: + resolution: {integrity: sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@0.5.1: + resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} + hasBin: true + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + + jsonrepair@3.13.3: + resolution: {integrity: sha512-BTznj0owIt2CBAH/LTo7+1I5pMvl1e1033LRl/HUowlZmJOIhzC0zbX5bxMngLkfT4WnzPP26QnW5wMr2g9tsQ==} + hasBin: true + + jsonwebtoken@8.5.1: + resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} + engines: {node: '>=4', npm: '>=1.4.28'} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + juice@9.1.0: + resolution: {integrity: sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==} + engines: {node: '>=10.0.0'} + hasBin: true + + just-extend@4.2.1: + resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} + + just-extend@6.2.0: + resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jwk-to-pem@2.0.7: + resolution: {integrity: sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==} + + jwks-rsa@3.2.0: + resolution: {integrity: sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==} + engines: {node: '>=14'} + + jws@3.2.3: + resolution: {integrity: sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + + keymaster@https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49: + resolution: {tarball: https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49} + version: 1.6.3 + + keypair@1.0.4: + resolution: {integrity: sha512-zwhgOhhniaL7oxMgUMKKw5219PWWABMO+dgMnzJOQ2/5L3XJtTJGhW2PEXlxXj9zaccdReZJZ83+4NPhVfNVDg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + knex-migrator@5.3.2: + resolution: {integrity: sha512-E8yLL2IWAni0yWWt1uq+x6qS80E+YVLQqwFFw1vkCQ+hq65MggHS3HjrZFXZqPv3cFrpyxHbdehwQoGQE3FC1g==} + engines: {node: ^14.18.0 || ^16.13.0 || ^18.12.1 || ^20.11.1 || ^22.13.1} + hasBin: true + + knex@0.20.15: + resolution: {integrity: sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q==} + engines: {node: '>=8'} + hasBin: true + peerDependencies: + mssql: ^6.1.0 + mysql: ^2.18.1 + mysql2: ^2.1.0 + pg: ^7.18.2 + sqlite3: ^4.1.1 + peerDependenciesMeta: + mssql: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + sqlite3: + optional: true + + knex@0.21.21: + resolution: {integrity: sha512-cjw5qO1EzVKjbywcVa61IQJMLt7PfYBRI/2NwCA/B9beXgbw652wDNLz+JM+UKKNsfwprq0ugYqBYc9q4JN36A==} + engines: {node: '>=10'} + hasBin: true + peerDependencies: + mssql: ^6.2.1 + mysql: ^2.18.1 + mysql2: ^2.1.0 + pg: ^8.3.0 + sqlite3: ^5.0.0 + peerDependenciesMeta: + mssql: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + sqlite3: + optional: true + + knex@2.4.2: + resolution: {integrity: sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + knex@3.1.0: + resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + known-css-properties@0.29.0: + resolution: {integrity: sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + lazy-universal-dotenv@4.0.0: + resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==} + engines: {node: '>=14.0.0'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lead@4.0.0: + resolution: {integrity: sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==} + engines: {node: '>=10.13.0'} + + leaky-bucket@2.2.0: + resolution: {integrity: sha512-87qsyt18gLVb+uB+zVz1zSi3yl6UJD5AoKINNOg3PBfqMis1FGgfOTi6hLkw7lJYZ3Gawf/BLj76WhDqsT0eZA==} + engines: {node: '>=v4.2'} + + leek@0.0.24: + resolution: {integrity: sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==} + + less-loader@11.1.4: + resolution: {integrity: sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + less: ^3.5.0 || ^4.0.0 + webpack: ^5.0.0 + + less@4.6.4: + resolution: {integrity: sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==} + engines: {node: '>=18'} + hasBin: true + + level-blobs@0.1.7: + resolution: {integrity: sha512-n0iYYCGozLd36m/Pzm206+brIgXP8mxPZazZ6ZvgKr+8YwOZ8/PPpYC5zMUu2qFygRN8RO6WC/HH3XWMW7RMVg==} + + level-filesystem@1.2.0: + resolution: {integrity: sha512-PhXDuCNYpngpxp3jwMT9AYBMgOvB6zxj3DeuIywNKmZqFj2djj9XfT2XDVslfqmo0Ip79cAd3SBy3FsfOZPJ1g==} + + level-fix-range@1.0.2: + resolution: {integrity: sha512-9llaVn6uqBiSlBP+wKiIEoBa01FwEISFgHSZiyec2S0KpyLUkGR4afW/FCZ/X8y+QJvzS0u4PGOlZDdh1/1avQ==} + + level-fix-range@2.0.0: + resolution: {integrity: sha512-WrLfGWgwWbYPrHsYzJau+5+te89dUbENBg3/lsxOs4p2tYOhCHjbgXxBAj4DFqp3k/XBwitcRXoCh8RoCogASA==} + + level-hooks@4.5.0: + resolution: {integrity: sha512-fxLNny/vL/G4PnkLhWsbHnEaRi+A/k8r5EH/M77npZwYL62RHi2fV0S824z3QdpAk6VTgisJwIRywzBHLK4ZVA==} + + level-js@2.2.4: + resolution: {integrity: sha512-lZtjt4ZwHE00UMC1vAb271p9qzg8vKlnDeXfIesH3zL0KxhHRDjClQLGLWhyR0nK4XARnd4wc/9eD1ffd4PshQ==} + deprecated: Superseded by browser-level (https://github.com/Level/community#faq) + + level-peek@1.0.6: + resolution: {integrity: sha512-TKEzH5TxROTjQxWMczt9sizVgnmJ4F3hotBI48xCTYvOKd/4gA/uY0XjKkhJFo6BMic8Tqjf6jFMLWeg3MAbqQ==} + + level-sublevel@5.2.3: + resolution: {integrity: sha512-tO8jrFp+QZYrxx/Gnmjawuh1UBiifpvKNAcm4KCogesWr1Nm2+ckARitf+Oo7xg4OHqMW76eAqQ204BoIlscjA==} + + levelup@0.18.6: + resolution: {integrity: sha512-uB0auyRqIVXx+hrpIUtol4VAPhLRcnxcOsd2i2m6rbFIDarO5dnrupLOStYYpEcu8ZT087Z9HEuYw1wjr6RL6Q==} + deprecated: Superseded by abstract-level (https://github.com/Level/community#faq) + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lexical@0.13.1: + resolution: {integrity: sha512-jaqRYzVEfBKbX4FwYpd/g+MyOjRaraAel0iQsTrwvx3hyN0bswUZuzb6H6nGlFSjcdrc77wKpyKwoWj4aUd+Bw==} + + libheif-js@1.19.8: + resolution: {integrity: sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==} + engines: {node: '>=8.0.0'} + + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + + liftoff@3.1.0: + resolution: {integrity: sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==} + engines: {node: '>= 0.8'} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + + line-column@1.0.2: + resolution: {integrity: sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + linkify-it@2.2.0: + resolution: {integrity: sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==} + + linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + liquid-fire@0.34.0: + resolution: {integrity: sha512-IppYBxIUhv00+Hi2Lno9kG4pcBUHVJD2ROQA7o9r0bLuLDfXTG94C8m6dwfYlswQBGdJAuN6a9ItLxftj8DgSw==} + engines: {node: 12.* || 14.* || >= 16} + peerDependencies: + ember-source: '>= 3.8.0' + + liquid-wormhole@3.0.1: + resolution: {integrity: sha512-DBnUTuvekd1zC9xQMm7/IY1g6UmfhRYgZOVKhCwIMdeDsS82Bk7pz/Qhr/Bg4ulV3xSqzcET7LRa7D3BKztoMA==} + engines: {node: 14.* || >= 16} + peerDependencies: + liquid-fire: ^0.34.0 + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + + livereload-js@3.4.1: + resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + loader-runner@2.4.0: + resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + + loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + loader.js@4.7.0: + resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + + locate-character@2.0.5: + resolution: {integrity: sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash._baseassign@3.2.0: + resolution: {integrity: sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==} + + lodash._basecopy@3.0.1: + resolution: {integrity: sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==} + + lodash._baseflatten@3.1.4: + resolution: {integrity: sha512-fESngZd+X4k+GbTxdMutf8ohQa0s3sJEHIcwtu4/LsIQ2JTDzdRxDCMQjW+ezzwRitLmHnacVVmosCbxifefbw==} + + lodash._baseiteratee@4.7.0: + resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} + + lodash._basetostring@4.12.0: + resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} + + lodash._baseuniq@4.6.0: + resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} + + lodash._bindcallback@3.0.1: + resolution: {integrity: sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==} + + lodash._createassigner@3.1.1: + resolution: {integrity: sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==} + + lodash._createset@4.0.3: + resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} + + lodash._getnative@3.9.1: + resolution: {integrity: sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==} + + lodash._isiterateecall@3.0.9: + resolution: {integrity: sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==} + + lodash._reinterpolate@3.0.0: + resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} + + lodash._root@3.0.1: + resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} + + lodash._stringtopath@4.8.0: + resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} + + lodash.assign@3.2.0: + resolution: {integrity: sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==} + + lodash.assignin@4.2.0: + resolution: {integrity: sha512-yX/rx6d/UTVh7sSVWVSIMjfnz95evAgDFdb1ZozC35I9mSFCkmzptOzevxjgbQUsc78NR44LVHWjsoMQXy9FDg==} + + lodash.bind@4.2.1: + resolution: {integrity: sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.debounce@3.1.1: + resolution: {integrity: sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.defaultsdeep@4.6.1: + resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.filter@4.6.0: + resolution: {integrity: sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==} + + lodash.flatten@3.0.2: + resolution: {integrity: sha512-jCXLoNcqQRbnT/KWZq2fIREHWeczrzpTR0vsycm96l/pu5hGeAntVBG0t7GuM/2wFqmnZs3d1eGptnAH2E8+xQ==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.foreach@4.5.0: + resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isarray@3.0.4: + resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.keys@3.1.2: + resolution: {integrity: sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==} + + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. + + lodash.reduce@4.6.0: + resolution: {integrity: sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==} + + lodash.reject@4.6.0: + resolution: {integrity: sha512-qkTuvgEzYdyhiJBx42YPzPo71R1aEr0z79kAv7Ixg8wPFEjgRgJdUsGMG3Hf3OYSF/kHI79XhNlt+5Ar6OzwxQ==} + + lodash.restparam@3.6.1: + resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.some@4.6.0: + resolution: {integrity: sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==} + + lodash.template@4.5.0: + resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} + deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. + + lodash.templatesettings@4.2.0: + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.uniqby@4.5.0: + resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + logd-console-output@1.3.0: + resolution: {integrity: sha512-Aau5xxpDXWBRIwl6zgConqmyNRBFq8VoQbzpQJjp9QVfrEtMgWUMbP2CUF9lCmMg0d2TQ79tEJA83+uQY36WAA==} + + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + longest-streak@2.0.4: + resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + engines: {node: 20 || >=22} + + lru-cache@2.7.3: + resolution: {integrity: sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + ltgt@2.2.1: + resolution: {integrity: sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==} + + lucide-react@0.577.0: + resolution: {integrity: sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.24.1: + resolution: {integrity: sha512-YBfNxbJiixMzxW40XqJEIldzHyh5f7CZKalo1uZffevyrPEX8Qgo9s0dmcORLHdV47UyvJg8/zD+6hQG3qvJrA==} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + mailgun.js@10.4.0: + resolution: {integrity: sha512-YrdaZEAJwwjXGBTfZTNQ1LM7tmkdUaz2NpZEu7+zULcG4Wrlhd7cWSNZW0bxT3bP48k5N0mZWz8C2f9gc2+Geg==} + engines: {node: '>=18.0.0'} + + mailgun.js@8.2.2: + resolution: {integrity: sha512-po/KtofzrTuKhHLenbmliDsVVOFANwcfDFUGnggwnyZJmZz7JgBlV6nzK9o2Fk+OK2SiBmJTK25RbkAj57Hd+Q==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-fetch-happen@15.0.5: + resolution: {integrity: sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==} + engines: {node: ^20.17.0 || >=22.9.0} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + make-iterator@1.0.1: + resolution: {integrity: sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==} + engines: {node: '>=0.10.0'} + + make-synchronized@0.8.0: + resolution: {integrity: sha512-DZu4lwc0ffoFz581BSQa/BJl+1ZqIkoRQ+VejMlH0VrP4E86StAODnZujZ4sepumQj8rcP7wUnUBGM8Gu+zKUA==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + map-or-similar@1.5.0: + resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + markdown-escapes@1.0.4: + resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==} + + markdown-it-footnote@4.0.0: + resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==} + + markdown-it-image-lazy-loading@2.0.1: + resolution: {integrity: sha512-IN1DNE2ubw8yn0I7G2conoxSz0R5GgMuOczbxtV+k20MtXeHrDvMQPkQYYGQSd+ejy4eSKQOhqu3gk676Ca//A==} + + markdown-it-lazy-headers@0.1.3: + resolution: {integrity: sha512-65BxqvmYLpVifv6MvTElthY8zvZ/TpZBCdshr/mTpsFkqwcwWtfD3YoSE7RYSn7ugnEAAaj2gywszq+hI/Pxgg==} + + markdown-it-mark@4.0.0: + resolution: {integrity: sha512-YLhzaOsU9THO/cal0lUjfMjrqSMPjjyjChYM7oyj4DnyaXEzA8gnW6cVJeyCrCVeyesrY2PlEdUYJSPFYL4Nkg==} + + markdown-it-sub@2.0.0: + resolution: {integrity: sha512-iCBKgwCkfQBRg2vApy9vx1C1Tu6D8XYo8NvevI3OlwzBRmiMtsJ2sXupBgEA7PPxiDwNni3qIUkhZ6j5wofDUA==} + + markdown-it-sup@2.0.0: + resolution: {integrity: sha512-5VgmdKlkBd8sgXuoDoxMpiU+BiEt3I49GItBzzw7Mxq9CxvnhE/k09HFli09zgfFDRixDQDfDxi0mgBCXtaTvA==} + + markdown-it-terminal@0.2.1: + resolution: {integrity: sha512-e8hbK9L+IyFac2qY05R7paP+Fqw1T4pSQW3miK3VeG9QmpqBjg5Qzjv/v6C7YNxSNRS2Kp8hUFtm5lWU9eK4lw==} + + markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + markdown-it@8.4.2: + resolution: {integrity: sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==} + hasBin: true + + markdown-table@1.1.3: + resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} + + match-media@0.2.0: + resolution: {integrity: sha512-EoLj8yVf95UNcZMbNJBlAA3E6XLzgMI6ZbnL90WnL2YCzOO4Nza1Ol6TH9ElbuUcDWxN7KxgVM2vYI6P5JqvKg==} + + matcher-collection@1.1.2: + resolution: {integrity: sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==} + + matcher-collection@2.0.1: + resolution: {integrity: sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==} + engines: {node: 6.* || 8.* || >= 10.*} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + mdast-util-compact@1.0.4: + resolution: {integrity: sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==} + + mdn-data@1.1.4: + resolution: {integrity: sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdn-data@2.0.4: + resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + memoizerific@1.11.3: + resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + + memory-fs@0.4.1: + resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} + + memory-fs@0.5.0: + resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + + memory-streams@0.1.3: + resolution: {integrity: sha512-qVQ/CjkMyMInPaaRMrwWNDvf6boRZXaT/DbQeMYcCWuXPEBf1v8qChOc9OlEVQp2uOvRXa1Qu30fLmKhY6NipA==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + + meow@10.1.5: + resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge-trees@1.0.1: + resolution: {integrity: sha512-O7TWwipLHhc9tErjq3WBvNP7I1g7Wgudl1ZkLqpT7F2MZy1yEdgnI9cpZZxBaqk+wJZu+2b9FE7D3ubUmGFHFA==} + + merge-trees@2.0.0: + resolution: {integrity: sha512-5xBbmqYBalWqmhYm51XlohhkmVOua3VAUrrWh8t9iOkaLpS6ifqm/UVuUjQCeDVJ9Vx3g2l6ihfkbLSTeKsHbw==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + merge@1.2.1: + resolution: {integrity: sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==} + + metascraper-author@5.45.10: + resolution: {integrity: sha512-S00rvCOqoHrM3WzTRzeIg/TP5Pi35IqtFjsmYIhiqIzTlHHHWvsV2ZBnHjPQkwyu90kADWQh3Cem2BYYk1fvDw==} + engines: {node: '>= 16'} + + metascraper-description@5.45.10: + resolution: {integrity: sha512-SfIq7mvcU6QvX6RfdIdzvY37UBf1T59wflbEiSlMVMutuAFsx+2JhvlImK5eMDdH1SEL4bmgkmvAiE/oQYXD0g==} + engines: {node: '>= 16'} + + metascraper-image@5.45.10: + resolution: {integrity: sha512-s7Kt3tGjVHmE2bYT+lGGFMZe4QeIYVwAdHvFT4JIRCDTDQaxCHfry4+HnGc6nnGiJHSyzgNX+4eZtzELP+pS4w==} + engines: {node: '>= 16'} + + metascraper-logo-favicon@5.42.0: + resolution: {integrity: sha512-Fk9OQn9WBpKOeIYHs+73z7jd8RoHxU+HtBHM4tY88lvHozEXA7qIUl07NM3rURLJo8vQNT7zXcJ1l7VCHDfqfQ==} + engines: {node: '>= 16'} + + metascraper-logo@5.45.10: + resolution: {integrity: sha512-HpIUuczIgefUgmlpDRLRzgYE4P0gD7Z2VsgSI5Qa8sgXqhEGC45UPn6J+LaQuhTaBirVNCHhgqX5uHG4EPDhAw==} + engines: {node: '>= 16'} + + metascraper-publisher@5.45.10: + resolution: {integrity: sha512-LW5TrmeA3TNPqTCaSmDrs+WuID56OJaqZZffIeG/VtataeoLBPr5rr1c5pN+YNsB8RqW7Y8HpNBDlxs4GdkNuA==} + engines: {node: '>= 16'} + + metascraper-title@5.45.10: + resolution: {integrity: sha512-GGlA7zqkHOe1mAIeRm+V3cHdvveqNO1vX6US8+COorRj9q0tyOMfBFnvmpZp6wE/bX8yGk/r1PVKOdDvAMzBTA==} + engines: {node: '>= 16'} + + metascraper-url@5.45.10: + resolution: {integrity: sha512-rlT5I1W06U/WPzM6CR5i+IIm9WYi6eyLGanLDD1C+pMBZqptSqWO9lWBDJQmqZGEuWKMqD7sMij9vZ+IZPBdFQ==} + engines: {node: '>= 16'} + + metascraper@5.45.15: + resolution: {integrity: sha512-TzJIMHkhfhEEuA18CBE+vyov1ESYbpuMzIc/LILHWwpdWWEwmABh/pEX1AD3dsSLCMbcDLQUd4imlhAS6yqxdw==} + engines: {node: '>= 16'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + microdiff@1.5.0: + resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==} + + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + microsoft-capitalize@1.0.7: + resolution: {integrity: sha512-5CQkknFQ7Wq54JWtrv6OydAhNcGEYHlsuhYN81S1rREJskZBfAwbseA+nTvMqH7tZlEzGFz4vwrybyaxIteqqw==} + engines: {node: '>= 10'} + + miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + + mime-db@1.25.0: + resolution: {integrity: sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.13: + resolution: {integrity: sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@3.0.0: + resolution: {integrity: sha512-PiVO95TKvhiwgSwg1IdLYlCTdul38yZxZMIcnDSFIBUm4BNZha2qpQ4GpJ++15bHoKDtrW2D69lMfFwdFYtNZQ==} + engines: {node: '>=8'} + + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mingo@2.5.3: + resolution: {integrity: sha512-Wb98QEQ/DaT+xPQFAX08mzM/Zz2eW1UIpKH132gXglakl2SKYBCQFzeiFygS/Hgzc9j9MDDjgouB9W7BMaLyaQ==} + + mini-css-extract-plugin@2.10.2: + resolution: {integrity: sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + mini-queue@0.0.14: + resolution: {integrity: sha512-DNh9Wn8U1jrmn1yVfpviwClyER/Y4ltgGbG+LF/KIdKJ8BEo2Q9jDDPG7tEhz6F/DTZ/ohv5D7AAXFVSFyP05Q==} + engines: {node: '>=4'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@0.3.0: + resolution: {integrity: sha512-WFX1jI1AaxNTZVOHLBVazwTWKaQjoykSzCBNXB72vDTCzopQGtyP91tKdFK5cv1+qMwPyiTu1HqUriqplI8pcA==} + deprecated: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@0.0.8: + resolution: {integrity: sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==} + + minimist@0.2.4: + resolution: {integrity: sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@2.9.0: + resolution: {integrity: sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + miragejs@0.1.48: + resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==} + engines: {node: 6.* || 8.* || >= 10.*} + + mississippi@3.0.0: + resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} + engines: {node: '>=4.0.0'} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + + mkdirp@0.5.1: + resolution: {integrity: sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + hasBin: true + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mktemp@2.0.2: + resolution: {integrity: sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g==} + engines: {node: 20 || 22 || 24} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + mobiledoc-dom-renderer@0.7.0: + resolution: {integrity: sha512-A+gT6D4Ru3DKY7ZYOBRORmwhRJ7rDj2vy75D2dWuZS5NgX0mCmGs0yN7qs48YlxvfCif8RFpYsaaPg6Kc3MdJg==} + + mobiledoc-dom-renderer@0.7.2: + resolution: {integrity: sha512-0vw/ybxCWXI0sIcBk9GVq3PMfVWJ4qpLWBal8ZoZVP/S5MMRjxFwyOctOfUmsY2dVi6BSvomp8yFqNloawwyig==} + + mobiledoc-text-renderer@0.4.0: + resolution: {integrity: sha512-+Tzfo0hhUFxS0n5FWZ0nf6WUrvnVmsxaIdq0CyeLYD1lk8oW2ml+6WLdeLlzKM5OYYi3PWV6NR9HCUG01cdvWQ==} + + mocha-slow-test-reporter@0.1.2: + resolution: {integrity: sha512-c970lxDVywGYUMoDhp8x718hEYkCHXmMNtS+rJDGrDahd1cizo67zrBkxVQULcw0QotZmvKSp0ZLhlIKkPSAFw==} + + mocha@11.7.5: + resolution: {integrity: sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + mocha@2.5.3: + resolution: {integrity: sha512-jNt2iEk9FPmZLzL+sm4FNyOIDYXf2wUU6L4Cc8OIKK/kzgMHKPi4YhTZqG4bW4kQVdIv6wutDybRhXfdnujA1Q==} + engines: {node: '>= 0.8.x'} + hasBin: true + + mock-knex@https://codeload.github.com/TryGhost/mock-knex/tar.gz/68948e11b0ea4fe63456098dfdc169bea7f62009: + resolution: {tarball: https://codeload.github.com/TryGhost/mock-knex/tar.gz/68948e11b0ea4fe63456098dfdc169bea7f62009} + version: 0.4.13 + peerDependencies: + knex: '> 0.8' + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + moment-timezone@0.5.45: + resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} + + moment@2.24.0: + resolution: {integrity: sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==} + + moo@0.5.3: + resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} + + morgan@1.10.1: + resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + engines: {node: '>= 0.8.0'} + + mout@1.2.4: + resolution: {integrity: sha512-mZb9uOruMWgn/fw28DG4/yE3Kehfk1zKCLhuDU2O3vlKdnBBr4XaOCqVTflJ5aODavGUPqFHZgrFX3NJVuxGhQ==} + + move-concurrently@1.0.1: + resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} + deprecated: This package is no longer supported. + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@0.7.1: + resolution: {integrity: sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.14: + resolution: {integrity: sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mv@2.1.1: + resolution: {integrity: sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==} + engines: {node: '>=0.8.0'} + + mysql2@3.14.1: + resolution: {integrity: sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==} + engines: {node: '>= 8.0'} + + mysql2@3.18.1: + resolution: {integrity: sha512-Mz3yJ+YqppZUFr6Q9tP0g9v3RTWGfLQ/J4RlnQ6CNrWz436+FDtFDICecsbWYwjupgfPpj8ZtNVMsCX79VKpLg==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + najax@1.0.7: + resolution: {integrity: sha512-JqBMguf2plv1IDqhOE6eebnTivjS/ej0C/Sw831jVc+dRQIMK37oyktdQCGAQtwpl5DikOWI2xGfIlBPSSLgXg==} + engines: {node: '>= 4.4.3'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nan@2.26.2: + resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==} + + nanoclone@0.2.1: + resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nconf@0.12.1: + resolution: {integrity: sha512-p2cfF+B3XXacQdswUYWZ0w6Vld0832A/tuqjLBu3H1sfUcby4N2oVbGhyuCkZv+t3iY3aiFEj7gZGqax9Q2c1w==} + engines: {node: '>= 0.4.0'} + + nconf@0.13.0: + resolution: {integrity: sha512-hJ/u2xCpA663h6xyOiztx3y+lg9eU0rdkwJ+c4FtiHo2g/gB0sjXtW31yTdMLWLOIj1gL2FcJMwfOqouuUK/Wg==} + engines: {node: '>= 0.4.0'} + + ncp@2.0.0: + resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==} + hasBin: true + + nearley@2.20.1: + resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} + hasBin: true + + needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + nise@4.1.0: + resolution: {integrity: sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==} + + nise@6.1.4: + resolution: {integrity: sha512-vSA4IpRHRWZkmotu61SvF45Jirq4CTLT3KKOWJPsPMtxtOBOlxcAlXfv/OrWxkzAJiCBrvdfWvGQjHT7r7+Qqg==} + + no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + nock@13.5.6: + resolution: {integrity: sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==} + engines: {node: '>= 10.13'} + + node-abi@3.89.0: + resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-dir@0.1.17: + resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} + engines: {node: '>= 0.10.5'} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-jose@2.2.0: + resolution: {integrity: sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==} + + node-libs-browser@2.2.1: + resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==} + + node-loggly-bulk@3.0.1: + resolution: {integrity: sha512-KrqNHqseUnGNGz4q1ZsnfD10W/KQkwFLpZjTHp98TPUXEqZ8zytEAugfFeiPMz/YMzgY4hqGI+6hsMIPhzaE4A==} + engines: {node: '>= 0.8.0'} + + node-machine-id@1.1.12: + resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + + node-modules-path@1.0.2: + resolution: {integrity: sha512-6Gbjq+d7uhkO7epaKi5DNgUJn7H0gEyA4Jg0Mo1uQOi3Rk50G83LtmhhFyw0LxnAFhtlspkiiw52ISP13qzcBg==} + + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + nodemailer-direct-transport@3.3.2: + resolution: {integrity: sha512-vEMLWdUZP9NpbeabM8VTiB3Ar1R0ixASp/6DdKX372LK4USKB4Lq12/WCp69k/+kWk4RiCWWEGo57CcsXOs/bw==} + + nodemailer-fetch@1.6.0: + resolution: {integrity: sha512-P7S5CEVGAmDrrpn351aXOLYs1R/7fD5NamfMCHyi6WIkbjS2eeZUB/TkuvpOQr0bvRZicVqo59+8wbhR3yrJbQ==} + + nodemailer-mailgun-transport@2.1.5: + resolution: {integrity: sha512-hF7POkaxFgMvYEd5aHLaQJI2511ld+aQlQi7JH6bGjhjlZ33cIbTB9PimlIrLu5XC3z76Kde6e65OIwL9lOdTA==} + + nodemailer-shared@1.1.0: + resolution: {integrity: sha512-68xW5LSyPWv8R0GLm6veAvm7E+XFXkVgvE3FW0FGxNMMZqMkPFeGDVALfR1DPdSfcoO36PnW7q5AAOgFImEZGg==} + + nodemailer-stub-transport@1.1.0: + resolution: {integrity: sha512-4fwl2f+647IIyuNuf6wuEMqK4oEU9FMJSYme8kPckVSr1rXIXcmI6BNcIWO+1cAK8XeexYKxYoFztam0jAwjkA==} + + nodemailer@6.10.1: + resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==} + engines: {node: '>=6.0.0'} + + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + + nopt@3.0.6: + resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@3.3.0: + resolution: {integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==} + engines: {node: '>=6'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + normalize-url@8.1.1: + resolution: {integrity: sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==} + engines: {node: '>=14.16'} + + normalize.css@3.0.3: + resolution: {integrity: sha512-7OavvKXqPta8bgGhWzcUOnZ9KCkANYzbq4ytNKgNeFRTaz3KAm1eGUQ/vyAeVjlDXjolsbYREctuzyQytCDiBA==} + + now-and-later@3.0.0: + resolution: {integrity: sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==} + engines: {node: '>= 10.13.0'} + + npm-git-info@1.0.3: + resolution: {integrity: sha512-i5WBdj4F/ULl16z9ZhsJDMl1EQCMQhHZzBwNnKL2LOA+T8IHNeRkLCVz9uVV9SzUdGTbDq+1oXhIYMe+8148vw==} + + npm-package-arg@8.1.5: + resolution: {integrity: sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==} + engines: {node: '>=10'} + + npm-run-all@4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@3.1.0: + resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} + engines: {node: '>=8'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + npmlog@4.1.2: + resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} + deprecated: This package is no longer supported. + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + nth-check@1.0.2: + resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + null-prototype-object@1.2.6: + resolution: {integrity: sha512-m+HuHAqaOiqlxVAzI3GtRgS4JIk3aAZ5RbkYxxPiACXyQZzS5uxjOXMASlWNdPYZ/5d4/+YclUlI+XXNlyN7Jg==} + engines: {node: '>= 20'} + + num2fraction@1.2.2: + resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} + + number-flow@0.5.8: + resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} + + number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + + numbered@1.1.0: + resolution: {integrity: sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + nx@22.0.4: + resolution: {integrity: sha512-yaKvr1MogUv3uh4s8VhSQh1l8mhtupclxVTTAua6bSaUYeuUT0qYn52w+Zf5ALg0YCtfzrNUeBgZK2l83HlkgA==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.8.0 + '@swc/core': ^1.3.85 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-hash@1.3.1: + resolution: {integrity: sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==} + engines: {node: '>= 0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@0.2.0: + resolution: {integrity: sha512-XODjdR2pBh/1qrjPcbSeSgEtKbYo7LqYNq64/TPuCf7j9SfDD3i21yatKoIy39yIWNvVM59iutfQQpCv1RfFzA==} + deprecated: Please update to the latest object-keys + + object-keys@0.4.0: + resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.defaults@1.1.0: + resolution: {integrity: sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==} + engines: {node: '>=0.10.0'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.getownpropertydescriptors@2.1.9: + resolution: {integrity: sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==} + engines: {node: '>= 0.4'} + + object.map@1.0.1: + resolution: {integrity: sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==} + engines: {node: '>=0.10.0'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + octal@1.0.0: + resolution: {integrity: sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optional-require@1.1.10: + resolution: {integrity: sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==} + engines: {node: '>=4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} + engines: {node: '>=10'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + osenv@0.1.5: + resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} + deprecated: This package is no longer supported. + + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-cancelable@4.0.1: + resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} + engines: {node: '>=14.16'} + + p-defer@3.0.0: + resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} + engines: {node: '>=8'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-finally@2.0.1: + resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} + engines: {node: '>=8'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + + p-reflect@2.1.0: + resolution: {integrity: sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + p-wait-for@3.1.0: + resolution: {integrity: sha512-0Uy19uhxbssHelu9ynDMcON6BmMk6pH8551CvxROhiz3Vx+yC4RqxjyIDk2V4ll0g9177RKT++PK4zcV58uJ7A==} + engines: {node: '>=8'} + + p-wait-for@3.2.0: + resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + papaparse@5.3.2: + resolution: {integrity: sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==} + + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + + parallel-transform@1.2.0: + resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} + + param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-asn1@5.1.9: + resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} + engines: {node: '>= 0.10'} + + parse-email-address@0.0.2: + resolution: {integrity: sha512-Ul8NS2MQ6PG7xdaa2SCNbkUjKcm0TTySXWquqDT8u+vldPVHZNhhrwfhG0KqN8caA96ul34Ptu6JtX7JYtK8CA==} + engines: {node: '>=20'} + + parse-entities@1.2.2: + resolution: {integrity: sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==} + + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@1.0.1: + resolution: {integrity: sha512-LpH1Cf5EYuVjkBvCDBYvkUPh+iv2bk3FHflxHkpCYT0/FZ1d3N3uJaLiHr4yGuMcFUhv6eAivitTvWZI4B/chg==} + engines: {node: '>=0.10.0'} + + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse-prometheus-text-format@1.1.1: + resolution: {integrity: sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA==} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse-static-imports@1.1.0: + resolution: {integrity: sha512-HlxrZcISCblEV0lzXmAHheH/8qEkKgmqkdxyHTPbSqsTUV8GzqmN1L+SSti+VbNPfbBO3bYLPHDiUs2avbAdbA==} + + parse-uri@2.0.5: + resolution: {integrity: sha512-E4J7siDZ1CL5EE/0YHX9y8IVNdSB/ZqjFPIKZWtWbSi5w8Q2h1OYjHSBezG15CsA6ieZmH5srrbpv0Rtf3I8rw==} + engines: {node: '>= 0.10'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + + parseley@0.7.0: + resolution: {integrity: sha512-xyOytsdDu077M3/46Am+2cGXEKM9U9QclBDv7fimY7e+BBlxh2JcBp2mgNsmkyA9uvgyTjVzDi7cP1v4hcFxbw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-browserify@0.0.1: + resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-dirname@1.0.2: + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-expression-matcher@1.2.0: + resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-match@1.2.4: + resolution: {integrity: sha512-UWlehEdqu36jmh4h5CWJ7tARp1OEVKGHKm6+dg9qMq5RKUTV5WJrGgaZ3dN2m7WFAXDbjlHzvJvL/IUpy84Ktw==} + deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-posix@1.0.0: + resolution: {integrity: sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==} + + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-to-regexp@8.4.0: + resolution: {integrity: sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pbkdf2@3.1.5: + resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} + engines: {node: '>= 0.10'} + + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perf-primitives@https://codeload.github.com/RobbieTheWagner/perf-primitives/tar.gz/a6a26f11497ca27be3763a88a5f20744e424756b: + resolution: {tarball: https://codeload.github.com/RobbieTheWagner/perf-primitives/tar.gz/a6a26f11497ca27be3763a88a5f20744e424756b} + version: 0.0.6 + engines: {node: 10.* || >= 12} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + pg-connection-string@2.1.0: + resolution: {integrity: sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg==} + + pg-connection-string@2.4.0: + resolution: {integrity: sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==} + + pg-connection-string@2.5.0: + resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} + + pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + + pkg-entry-points@1.1.1: + resolution: {integrity: sha512-BhZa7iaPmB4b3vKIACoppyUoYn8/sFs17VJJtzrzPZvEnN2nqrgg911tdL65lA2m1ml6UI3iPeYbZQ4VXpn1mA==} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + pkg-up@2.0.0: + resolution: {integrity: sha512-fjAPuiws93rm7mPUu21RdBnkeZNrbfCFCwfAhPWY+rR3zG0ubpe5cEReHOw5fIbfmsxEV/g2kSxGTATY3Bpnwg==} + engines: {node: '>=4'} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + + popper.js@1.16.1: + resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} + deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + + portfinder@1.0.38: + resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} + engines: {node: '>= 10.12'} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-attribute-case-insensitive@5.0.2: + resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-calc@10.1.1: + resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} + engines: {node: ^18.12 || ^20.9 || >=22.0} + peerDependencies: + postcss: ^8.4.38 + + postcss-calc@7.0.5: + resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} + + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + + postcss-cli@11.0.1: + resolution: {integrity: sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + postcss: ^8.0.0 + + postcss-color-functional-notation@4.2.4: + resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-color-hex-alpha@8.0.4: + resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-color-mod-function@3.0.3: + resolution: {integrity: sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==} + engines: {node: '>=6.0.0'} + + postcss-color-rebeccapurple@7.1.1: + resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-colormin@4.0.3: + resolution: {integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==} + engines: {node: '>=6.9.0'} + + postcss-colormin@7.0.6: + resolution: {integrity: sha512-oXM2mdx6IBTRm39797QguYzVEWzbdlFiMNfq88fCCN1Wepw3CYmJ/1/Ifa/KjWo+j5ZURDl2NTldLJIw51IeNQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-convert-values@4.0.1: + resolution: {integrity: sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==} + engines: {node: '>=6.9.0'} + + postcss-convert-values@7.0.9: + resolution: {integrity: sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-custom-media@7.0.8: + resolution: {integrity: sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==} + engines: {node: '>=6.0.0'} + + postcss-custom-media@8.0.2: + resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + + postcss-custom-properties@10.0.0: + resolution: {integrity: sha512-55BPj5FudpCiPZzBaO+MOeqmwMDa+nV9/0QBJBfhZjYg6D9hE+rW9lpMBLTJoF4OTXnS5Po4yM1nMlgkPbCxFg==} + engines: {node: '>=10.0.0'} + + postcss-custom-properties@12.1.11: + resolution: {integrity: sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-custom-selectors@6.0.3: + resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + + postcss-dir-pseudo-class@6.0.5: + resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-discard-comments@4.0.2: + resolution: {integrity: sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==} + engines: {node: '>=6.9.0'} + + postcss-discard-comments@7.0.6: + resolution: {integrity: sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-discard-duplicates@4.0.2: + resolution: {integrity: sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==} + engines: {node: '>=6.9.0'} + + postcss-discard-duplicates@7.0.2: + resolution: {integrity: sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-discard-empty@4.0.1: + resolution: {integrity: sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==} + engines: {node: '>=6.9.0'} + + postcss-discard-empty@7.0.1: + resolution: {integrity: sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-discard-overridden@4.0.1: + resolution: {integrity: sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==} + engines: {node: '>=6.9.0'} + + postcss-discard-overridden@7.0.1: + resolution: {integrity: sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-double-position-gradients@3.1.2: + resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-env-function@4.0.6: + resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-focus-visible@6.0.4: + resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@5.0.4: + resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@3.0.5: + resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-image-set-function@4.0.7: + resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-import@12.0.1: + resolution: {integrity: sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==} + engines: {node: '>=6.0.0'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-import@16.1.1: + resolution: {integrity: sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-initial@4.0.1: + resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-lab-function@4.2.1: + resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-load-config@5.1.0: + resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-loader@7.3.4: + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-logical@5.0.4: + resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + + postcss-media-minmax@5.0.0: + resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.1.0 + + postcss-merge-longhand@4.0.11: + resolution: {integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==} + engines: {node: '>=6.9.0'} + + postcss-merge-longhand@7.0.5: + resolution: {integrity: sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-merge-rules@4.0.3: + resolution: {integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==} + engines: {node: '>=6.9.0'} + + postcss-merge-rules@7.0.8: + resolution: {integrity: sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-font-values@4.0.2: + resolution: {integrity: sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==} + engines: {node: '>=6.9.0'} + + postcss-minify-font-values@7.0.1: + resolution: {integrity: sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-gradients@4.0.2: + resolution: {integrity: sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==} + engines: {node: '>=6.9.0'} + + postcss-minify-gradients@7.0.1: + resolution: {integrity: sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-params@4.0.2: + resolution: {integrity: sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==} + engines: {node: '>=6.9.0'} + + postcss-minify-params@7.0.6: + resolution: {integrity: sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-minify-selectors@4.0.2: + resolution: {integrity: sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==} + engines: {node: '>=6.9.0'} + + postcss-minify-selectors@7.0.6: + resolution: {integrity: sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nesting@10.2.0: + resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-normalize-charset@4.0.1: + resolution: {integrity: sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==} + engines: {node: '>=6.9.0'} + + postcss-normalize-charset@7.0.1: + resolution: {integrity: sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-display-values@4.0.2: + resolution: {integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==} + engines: {node: '>=6.9.0'} + + postcss-normalize-display-values@7.0.1: + resolution: {integrity: sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-positions@4.0.2: + resolution: {integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-positions@7.0.1: + resolution: {integrity: sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-repeat-style@4.0.2: + resolution: {integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==} + engines: {node: '>=6.9.0'} + + postcss-normalize-repeat-style@7.0.1: + resolution: {integrity: sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-string@4.0.2: + resolution: {integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-string@7.0.1: + resolution: {integrity: sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-timing-functions@4.0.2: + resolution: {integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==} + engines: {node: '>=6.9.0'} + + postcss-normalize-timing-functions@7.0.1: + resolution: {integrity: sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-unicode@4.0.1: + resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==} + engines: {node: '>=6.9.0'} + + postcss-normalize-unicode@7.0.6: + resolution: {integrity: sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-url@4.0.1: + resolution: {integrity: sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-url@7.0.1: + resolution: {integrity: sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-normalize-whitespace@4.0.2: + resolution: {integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-whitespace@7.0.1: + resolution: {integrity: sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-opacity-percentage@1.1.3: + resolution: {integrity: sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-ordered-values@4.1.2: + resolution: {integrity: sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==} + engines: {node: '>=6.9.0'} + + postcss-ordered-values@7.0.2: + resolution: {integrity: sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-overflow-shorthand@3.0.4: + resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@7.0.5: + resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-preset-env@7.8.3: + resolution: {integrity: sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-pseudo-class-any-link@7.1.6: + resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-reduce-initial@4.0.3: + resolution: {integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==} + engines: {node: '>=6.9.0'} + + postcss-reduce-initial@7.0.6: + resolution: {integrity: sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-reduce-transforms@4.0.2: + resolution: {integrity: sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==} + engines: {node: '>=6.9.0'} + + postcss-reduce-transforms@7.0.1: + resolution: {integrity: sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-reporter@7.1.0: + resolution: {integrity: sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.1.0 + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-selector-not@6.0.1: + resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + + postcss-selector-parser@3.1.2: + resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==} + engines: {node: '>=8'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-svgo@4.0.3: + resolution: {integrity: sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==} + engines: {node: '>=6.9.0'} + + postcss-svgo@7.1.1: + resolution: {integrity: sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==} + engines: {node: ^18.12.0 || ^20.9.0 || >= 18} + peerDependencies: + postcss: ^8.4.32 + + postcss-unique-selectors@4.0.1: + resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==} + engines: {node: '>=6.9.0'} + + postcss-unique-selectors@7.0.5: + resolution: {integrity: sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss-values-parser@2.0.1: + resolution: {integrity: sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==} + engines: {node: '>=6.14.4'} + + postcss-values-parser@4.0.0: + resolution: {integrity: sha512-R9x2D87FcbhwXUmoCXJR85M1BLII5suXRuXibGYyBJ7lVDEpRIdKZh4+8q5S+/+A4m0IoG1U5tFw39asyhX/Hw==} + engines: {node: '>=10'} + + postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretender@3.4.7: + resolution: {integrity: sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw==} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + pretty-hrtime@1.0.3: + resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} + engines: {node: '>= 0.8'} + + pretty-ms@3.2.0: + resolution: {integrity: sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q==} + engines: {node: '>=4'} + + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prettyjson@1.2.5: + resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} + hasBin: true + + printf@0.6.1: + resolution: {integrity: sha512-is0ctgGdPJ5951KulgfzvHGwJtZ5ck8l042vRkV6jrkpBzTmb/lueTqguWHy2JfVA+RY6gFVlaZgUS0j7S/dsw==} + engines: {node: '>= 0.9.0'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + private@0.1.8: + resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} + engines: {node: '>= 0.6'} + + probability-distributions@0.9.1: + resolution: {integrity: sha512-guES77A321GzcK6swYmxhBeYQ10UNyiG0Gf4Ks+3239BuS27EFH/rrlfBW/oYlPyiufdey/3OlVqKvlXFB9LTQ==} + + probe-image-size@7.2.3: + resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} + + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-es6@0.11.6: + resolution: {integrity: sha512-GYBRQtL4v3wgigq10Pv58jmTbFXlIiTbSfgnNqZLY0ldUPqy1rRxDI5fCjoCpnM6TqmHQI8ydzTBXW86OYc0gA==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-relative-require@1.0.0: + resolution: {integrity: sha512-r8G5WJPozMJAiv8sDdVWKgJ4In/zBXqwJdMCGAXQt2Kd3HdbAuJVzWYM4JW150hWoaI9DjhtbjcsCCHIMxm8RA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-map-series@0.2.3: + resolution: {integrity: sha512-wx9Chrutvqu1N/NHzTayZjE1BgIwt6SJykQoCOic4IZ9yUDjKyVYrpLa/4YCNsV61eRENfs29hrEquVuB13Zlw==} + + promise-map-series@0.3.0: + resolution: {integrity: sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==} + engines: {node: 10.* || >= 12.*} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + promise.hash.helper@1.0.8: + resolution: {integrity: sha512-KYcnXctWUWyVD3W3Ye0ZDuA1N8Szrh85cVCxpG6xYrOk/0CttRtYCmU30nWsUch0NuExQQ63QXvzRE6FLimZmg==} + engines: {node: 10.* || >= 12.*} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + propagate@2.0.1: + resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} + engines: {node: '>= 8'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + + prosemirror-changeset@2.4.0: + resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.4: + resolution: {integrity: sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==} + + prosemirror-menu@1.3.0: + resolution: {integrity: sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==} + + prosemirror-model@1.25.4: + resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.11.0: + resolution: {integrity: sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==} + + prosemirror-view@1.41.7: + resolution: {integrity: sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + prr@0.0.0: + resolution: {integrity: sha512-LmUECmrW7RVj6mDWKjTXfKug7TFGdiz9P18HMcO4RHL+RW7MCOGNvpj5j47Rnp6ne6r4fZ2VzyUWEpKbg+tsjQ==} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + + pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode2@1.0.1: + resolution: {integrity: sha512-+TXpd9YRW4YUZZPoRHJ3DILtWwootGc2DsgvfHmklQ8It1skINAuqSdqizt5nlTaBmwrYACHkHApCXjc9gHk2Q==} + engines: {node: '>=0.10'} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + qs@6.5.5: + resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + quick-temp@0.1.9: + resolution: {integrity: sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA==} + + raf-pool@0.1.4: + resolution: {integrity: sha512-BBPamTVuSprPq7CUmgxc+ycbsYUtUYnQtJYEfMHXMaostPaNpQzipLfSa/rwjmlgjBPiD7G+I+8W340sLOPu6g==} + + railroad-diagrams@1.0.0: + resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + + ramda@0.27.2: + resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==} + + ramda@0.29.0: + resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} + + randexp@0.4.6: + resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} + engines: {node: '>=0.12'} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@1.1.7: + resolution: {integrity: sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==} + engines: {node: '>= 0.8.0'} + deprecated: No longer maintained. Please upgrade to a stable version. + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + re2@1.23.3: + resolution: {integrity: sha512-5jh686rmj/8dYpBo72XYgwzgG8Y9HNDATYZ1x01gqZ6FvXVUP33VZ0+6GLCeavaNywz3OkXBU8iNX7LjiuisPg==} + + reachable-url@1.7.2: + resolution: {integrity: sha512-aJHwaTbbLjYcbMmzD/6xQ785vbEs8TTwoUg/Y/+faSk0TxPcsUxFk9bwsYRyDJID/krm1ZATBn3U+JSP1Vv2Bg==} + engines: {node: '>=8'} + + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + react-docgen-typescript@2.4.0: + resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@7.1.1: + resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} + engines: {node: '>=16.14.0'} + + react-docgen@8.0.3: + resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + engines: {node: ^20.9.0 || >=22} + + react-dom@17.0.2: + resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} + peerDependencies: + react: 17.0.2 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@14.2.3: + resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + + react-hook-form@7.72.1: + resolution: {integrity: sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router@7.14.0: + resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-smooth@4.0.4: + resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-string-replace@1.1.1: + resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==} + engines: {node: '>=0.12.0'} + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-svg-map@2.2.0: + resolution: {integrity: sha512-slZTt4ffXgOb3ND/WEHOc0ojX5lBEv9FKbTU3tWnTLZPQ9L0e686SBqgVxmsuTD+o52Aor87InMzAzDRTQedpA==} + peerDependencies: + react: ^16.0.0 + react-dom: ^16.0.0 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-world-flags@1.6.0: + resolution: {integrity: sha512-eutSeAy5YKoVh14js/JUCSlA6EBk1n4k+bDaV+NkNB50VhnG+f4QDTpYycnTUTsZ5cqw/saPmk0Z4Fa0VVZ1Iw==} + peerDependencies: + react: '>=0.14' + + react@17.0.2: + resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} + engines: {node: '>=0.10.0'} + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg-up@8.0.0: + resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} + engines: {node: '>=12'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read-pkg@6.0.0: + resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} + engines: {node: '>=12'} + + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + + readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@2.2.1: + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + recast@0.18.10: + resolution: {integrity: sha512-XNvYvkfdAN9QewbrxeTOjgINkdY/odTgTS56ZNEWL9Ml0weT4T3sFtvnTuF+Gxyu46ANcRm1ntrF6F5LAJPAaQ==} + engines: {node: '>= 4'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.15.4: + resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redent@4.0.0: + resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} + engines: {node: '>=12'} + + redeyed@1.0.1: + resolution: {integrity: sha512-8eEWsNCkV2rvwKLS1Cvp5agNjMhwRe2um+y32B2+3LqOzg4C9BBPs6vzAfV16Ivb8B9HPNKIqd8OrdBws8kNlQ==} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + reframe.js@4.0.2: + resolution: {integrity: sha512-cWeYXeAdPIZoL9U+hrsALRwjaMbZR0/tFDh9xF2JmTUCSHi2OaeioC91Exmc00C3jpmEj41++2UnNZXIXL5pMA==} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.10.5: + resolution: {integrity: sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==} + + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regenerator-runtime@0.9.6: + resolution: {integrity: sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw==} + + regenerator-transform@0.10.1: + resolution: {integrity: sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + regex-parser@2.3.1: + resolution: {integrity: sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@2.0.0: + resolution: {integrity: sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.2.0: + resolution: {integrity: sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.1.5: + resolution: {integrity: sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==} + hasBin: true + + regjsparser@0.13.0: + resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + remark-footnotes@1.0.0: + resolution: {integrity: sha512-X9Ncj4cj3/CIvLI2Z9IobHtVi8FVdUrdJkCNaL9kdX8ohfsi18DXHsCVd/A7ssARBdccdDb5ODnt62WuEWaM/g==} + + remark-parse@7.0.2: + resolution: {integrity: sha512-9+my0lQS80IQkYXsMA8Sg6m9QfXYJBnXjWYN5U+kFc5/n69t+XZVXU/ZBYr3cYH8FheEGf1v87rkFDhJ8bVgMA==} + + remark-stringify@7.0.4: + resolution: {integrity: sha512-qck+8NeA1D0utk1ttKcWAoHRrJxERYQzkHDyn+pF5Z4whX1ug98uCNPPSeFgLSaNERRxnD6oxIug6DzZQth6Pg==} + + remark@11.0.2: + resolution: {integrity: sha512-bh+eJgn8wgmbHmIBOuwJFdTVRVpl3fcVP6HxmpPWO0ULGP9Qkh6INJh0N5Uy7GqlV7DQYGoqaKiEIpM5LLvJ8w==} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + repeating@2.0.1: + resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} + engines: {node: '>=0.10.0'} + + replace-ext@2.0.0: + resolution: {integrity: sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==} + engines: {node: '>= 10'} + + reqresnext@1.7.0: + resolution: {integrity: sha512-TS/B8FKRf7j1oowoAYB1Hv+Ga92Y6vY6NvxVF0fqw11KFdwe+DI6jSWB7XHa1Hf21lRqT6vmwy+6KOkLBF6GkQ==} + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + + require-at@1.0.6: + resolution: {integrity: sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + require-relative@0.8.7: + resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==} + + requireindex@1.1.0: + resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} + engines: {node: '>=0.10.5'} + + requireindex@1.2.0: + resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} + engines: {node: '>=0.10.5'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@3.0.1: + resolution: {integrity: sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==} + + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-options@2.0.0: + resolution: {integrity: sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==} + engines: {node: '>= 10.13.0'} + + resolve-package-path@1.2.7: + resolution: {integrity: sha512-fVEKHGeK85bGbVFuwO9o1aU0n3vqQGrezPc51JGu9UTXpFQfWq5qCeKxyaRUSvephs+06c5j5rPq/dzHGEo8+Q==} + + resolve-package-path@2.0.0: + resolution: {integrity: sha512-/CLuzodHO2wyyHTzls5Qr+EFeG6RcW4u6//gjYvUfcfyuplIX1SSccU+A5A9A78Gmezkl3NBkFAMxLbzTY9TJA==} + engines: {node: 8.* || 10.* || >= 12} + + resolve-package-path@3.1.0: + resolution: {integrity: sha512-2oC2EjWbMJwvSN6Z7DbDfJMnD8MYEouaLn5eIX0j8XwPsYCVIyY9bbnX88YHVkbr8XHqvZrYbxaLPibfTYKZMA==} + engines: {node: 10.* || >= 12} + + resolve-package-path@4.0.3: + resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} + engines: {node: '>= 12'} + + resolve-path@1.4.0: + resolution: {integrity: sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==} + engines: {node: '>= 0.8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-url-loader@5.0.0: + resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==} + engines: {node: '>=12'} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + responselike@4.0.2: + resolution: {integrity: sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==} + engines: {node: '>=20'} + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rewire@9.0.1: + resolution: {integrity: sha512-dnbLeTwHpXvWJjswC6CshXUUnnpE5AVhlayVRvDJhJx5ejbO4nbj1IXqN2urErgB7TpHUAMpf6iPDhQIxeSQOQ==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rgb-regex@1.0.1: + resolution: {integrity: sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==} + + rgba-regex@1.0.0: + resolution: {integrity: sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==} + + rimraf@2.4.5: + resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + + rollup-plugin-node-builtins@2.1.2: + resolution: {integrity: sha512-bxdnJw8jIivr2yEyt8IZSGqZkygIJOGAWypXvHXnwKAbUcN4Q/dGTx7K0oAJryC/m6aq6tKutltSeXtuogU6sw==} + + rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + + rollup@0.57.1: + resolution: {integrity: sha512-I18GBqP0qJoJC1K1osYjreqA8VAKovxuI3I81RSk0Dmr4TgloI0tAULjZaox8OsJ+n7XRrhH6i0G2By/pj1LCA==} + hasBin: true + + rollup@1.32.1: + resolution: {integrity: sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==} + hasBin: true + + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + route-recognizer@0.3.4: + resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rss@1.2.2: + resolution: {integrity: sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==} + + rsvp@3.2.1: + resolution: {integrity: sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==} + + rsvp@3.6.2: + resolution: {integrity: sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==} + engines: {node: 0.12.* || 4.* || 6.* || >= 7.*} + + rsvp@4.8.5: + resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} + engines: {node: 6.* || >= 7.*} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + run-queue@1.0.3: + resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-json-parse@1.0.1: + resolution: {integrity: sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==} + + safe-json-stringify@1.2.0: + resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safe-timers@1.1.0: + resolution: {integrity: sha512-9aqY+v5eMvmRaluUEtdRThV1EjlSElzO7HuCj0sTW9xvp++8iJ9t/RWGNWV6/WHcUJLHpyT2SNf/apoKTU2EpA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sane@4.1.0: + resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added + hasBin: true + + sanitize-html@2.17.0: + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + + sass-loader@13.3.3: + resolution: {integrity: sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.20.2: + resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@1.0.0: + resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==} + engines: {node: '>= 4'} + + schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + section-tests@1.3.1: + resolution: {integrity: sha512-cZFz5XcvYzdHUCOwwrrSFPA9KZdukGENO7ukOgxpcScZaM8zRMIyR6LRySdgoX0nD7NH+XY85F1pQ7n89CZliw==} + hasBin: true + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + secure-keys@1.0.0: + resolution: {integrity: sha512-nZi59hW3Sl5P3+wOO89eHBAAGwmCPd2aE1+dLZV5MO+ItQctIvAqihzaAXIQhvtH4KJPxM080HsnqltR2y8cWg==} + + selderee@0.6.0: + resolution: {integrity: sha512-ibqWGV5aChDvfVdqNYuaJP/HnVBhlRGSRrlbttmlMpHcLuTqqbMH36QkSs9GEgj5M88JDYLI8eyP94JaQ8xRlg==} + + semver@2.3.2: + resolution: {integrity: sha512-abLdIKCosKfpnmhS52NCTjO4RiLspDfsn37prjzGrp9im5DPJOgh82Os92vtwGh6XdQryKI/7SREZnV+aqiXrA==} + hasBin: true + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + sentry-testkit@5.0.10: + resolution: {integrity: sha512-wpvt29IIK+L1Pe02uc0y7rJkXQcnIk46hvB02Zqd4UNgP2lIhzv80GjR2lu8RtsbAmf7LJnGz8zLuV3rVa69ew==} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sigmund@1.0.1: + resolution: {integrity: sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + silent-error@1.1.1: + resolution: {integrity: sha512-n4iEKyNcg4v6/jpb3c0/iyH2G1nzUNl7Gpqtn/mHIJK9S/q/7MCfoO4rwVOoO59qPFIc0hVHvMbiOJ0NdtxKKw==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-dom@1.4.0: + resolution: {integrity: sha512-TnBPkmOyjdaOqyBMb4ick+n8c0Xv9Iwg1PykFV7hz9Se3UCiacTbRb+25cPmvozFNJLBUNvUzX/KsPfXF14ivA==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-html-tokenizer@0.5.11: + resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sinon-chai@4.0.1: + resolution: {integrity: sha512-xMKEEV3cYHC1G+boyr7QEqi80gHznYsxVdC9CdjP5JnCWz/jPGuXQzJz3PtBcb0CcHAxar15Y5sjLBoAs6a0yA==} + peerDependencies: + chai: ^5.0.0 || ^6.0.0 + sinon: '>=4.0.0' + + sinon@18.0.1: + resolution: {integrity: sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==} + + sinon@21.0.1: + resolution: {integrity: sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==} + + sinon@21.1.1: + resolution: {integrity: sha512-Tn8sLJZay8gRFQycxVwgL86PSUt9SnS9N2wCg12XyR75BssSS1c2fBPFUzwDj7XTNZlaM8+qkETTBYWnkKWXwg==} + + sinon@9.2.4: + resolution: {integrity: sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==} + deprecated: 16.1.1 + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@1.0.0: + resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==} + engines: {node: '>=0.10.0'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smartquotes@2.3.2: + resolution: {integrity: sha512-0R6YJ5hLpDH4mZR7N5eZ12oCMLspvGOHL9A9SEm2e3b/CQmQidekW4SWSKEmor/3x6m3NCBBEqLzikcZC9VJNQ==} + engines: {node: '>=4.0.0'} + + smtp-connection@2.12.0: + resolution: {integrity: sha512-UP5jK4s5SGcUcqPN4U9ingqKt9mXYSKa52YhqxPuMecAnUOsVJpOmtgGaOm1urUBJZlzDt1M9WhZZkgbhxQlvg==} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + sort-keys@5.1.0: + resolution: {integrity: sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ==} + engines: {node: '>=12'} + + sort-object-keys@1.1.3: + resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} + + sort-package-json@1.57.0: + resolution: {integrity: sha512-FYsjYn2dHTRb41wqnv+uEqCUvBpK3jZcTp9rbz2qDTmel7Pmdtf+i2rLaaPMRZeSVM60V3Se31GyWFpmKs4Q5Q==} + hasBin: true + + source-list-map@2.0.1: + resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-support@0.4.18: + resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map-url@0.3.0: + resolution: {integrity: sha512-QU4fa0D6aSOmrT+7OHpUXw+jS84T0MLaQNtFs8xzLNe6Arj44Magd7WEbyVW5LNYoAPVV35aKs4azxIfVJrToQ==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + + source-map@0.4.4: + resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} + engines: {node: '>=0.8.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + sourcemap-validator@1.1.1: + resolution: {integrity: sha512-pq6y03Vs6HUaKo9bE0aLoksAcpeOo9HZd7I8pI6O480W/zxNZ9U32GfzgtPP0Pgc/K1JHna569nAbOk3X8/Qtw==} + engines: {node: ^0.10 || ^4.5 || 6.* || >= 7.*} + + spawn-args@0.2.0: + resolution: {integrity: sha512-73BoniQDcRWgnLAf/suKH6V5H54gd1KLzwYN9FB6J/evqTV33htH9xwV/4BHek+++jzxpVlZQKKZkqstPQPmQg==} + + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sql-escaper@1.3.3: + resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + ssri@6.0.2: + resolution: {integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stagehand@1.0.1: + resolution: {integrity: sha512-GqXBq2SPWv9hTXDFKS8WrKK1aISB0aKGHZzH+uD4ShAgs+Fz20ZfoerLOm8U+f62iRWLrw6nimOY/uYuTcVhvg==} + engines: {node: 6.* || 8.* || >= 10.*} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + state-toggle@1.0.3: + resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + + store2@2.14.4: + resolution: {integrity: sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==} + + storybook@10.3.3: + resolution: {integrity: sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + storybook@8.6.15: + resolution: {integrity: sha512-Ob7DMlwWx8s7dMvcQ3xPc02TvUeralb+xX3oaPRk9wY9Hc6M1IBC/7cEoITkSmRS2v38DHubC+mtEKNc1u2gQg==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + stream-browserify@2.0.2: + resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} + + stream-composer@1.0.2: + resolution: {integrity: sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==} + + stream-each@1.2.3: + resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==} + + stream-http@2.8.3: + resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} + + stream-parser@0.3.1: + resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-length@1.0.1: + resolution: {integrity: sha512-MNCACnufWUf3pQ57O5WTBMkKhzYIaKEcUioO0XHrTMafrbBaNk4IyDOLHBv5xbXO0jLLdsYWeFjpjG2hVHRDtw==} + engines: {node: '>=0.10.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-range@1.2.2: + resolution: {integrity: sha512-tYft6IFi8SjplJpxCUxyqisD3b+R2CSkomrtJYCkvuf1KuCAWgz7YXt4O0jip7efpfCemwHEzTEAO8EuOYgh3w==} + + string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + + string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.padend@3.1.6: + resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@2.0.0: + resolution: {integrity: sha512-fqqhZzXyAM6pGD9lky/GOPq6V4X0SeTAFBl0iXb/BzOegl40gpf/bV3QQP7zULNYvjr6+Dx8SCaDULjVoOru0A==} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stripe@8.222.0: + resolution: {integrity: sha512-hrA79fjmN2Eb6K3kxkDzU4ODeVGGjXQsuVaAPSUro6I9MM3X+BvIsVqdphm3BXWfimAGFvUqWtPtHy25mICY1w==} + engines: {node: ^8.1 || >=10.*} + + strnum@2.2.2: + resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==} + + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + + style-loader@2.0.0: + resolution: {integrity: sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + + styled_string@0.0.1: + resolution: {integrity: sha512-DU2KZiB6VbPkO2tGSqQ9n96ZstUPjW7X4sGO6V2m1myIQluX0p1Ol8BrA/l6/EesqhMqXOIXs3cJNOy1UuU2BA==} + + stylehacks@4.0.3: + resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} + engines: {node: '>=6.9.0'} + + stylehacks@7.0.8: + resolution: {integrity: sha512-I3f053GBLIiS5Fg6OMFhq/c+yW+5Hc2+1fgq7gElDMMSqwlRb3tBf2ef6ucLStYRpId4q//bQO1FjcyNyy4yDQ==} + engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} + peerDependencies: + postcss: ^8.4.32 + + stylelint@15.11.0: + resolution: {integrity: sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + sum-up@1.0.3: + resolution: {integrity: sha512-zw5P8gnhiqokJUWRdR6F4kIIIke0+ubQSGyYUY506GCbJWtV7F6Xuy0j6S125eSX2oF+a8KdivsZ8PlVEH0Mcw==} + + superagent-throttle@1.0.1: + resolution: {integrity: sha512-m5Ngf0S5QoA84zgwVqVnVA34u9uvo8uM+QEF9B7eNI5FDaSoSoUwQsx7V1GmLXgYLkolhIiucFDVJXF9z49hgQ==} + + superagent@5.3.1: + resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} + engines: {node: '>= 7.0.0'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + + superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net + + supertest@6.3.4: + resolution: {integrity: sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==} + engines: {node: '>=6.4.0'} + deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net + + supports-color@1.2.0: + resolution: {integrity: sha512-mS5xsnjTh5b7f2DM6bch6lR582UCOTphzINlZnDsfpIRrwI6r58rb6YSSGsdexkm8qw2bBVO2ID2fnJOTuLiPA==} + engines: {node: '>=0.10.0'} + hasBin: true + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-country-flags@1.2.10: + resolution: {integrity: sha512-xrqwo0TYf/h2cfPvGpjdSuSguUbri4vNNizBnwzoZnX0xGo3O5nGJMlbYEp7NOYcnPGBm6LE2axqDWSB847bLw==} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@1.3.0: + resolution: {integrity: sha512-MLfUA6O+qauLDbym+mMZgtXCGRfIxyQoeH6IKVcFslyODEe/ElJNwr0FohQ3xG4C6HK6bk3KYPPXwHVJk3V5NQ==} + engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. + hasBin: true + + svgo@1.3.2: + resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} + engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. + hasBin: true + + svgo@3.3.3: + resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==} + engines: {node: '>=14.0.0'} + hasBin: true + + svgo@4.0.1: + resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + engines: {node: '>=16'} + hasBin: true + + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + symlink-or-copy@1.3.1: + resolution: {integrity: sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==} + + sync-disk-cache@1.3.4: + resolution: {integrity: sha512-GlkGeM81GPPEKz/lH7QUTbvqLq7K/IUTuaKDSMulP9XQ42glqNJIN/RKgSOw4y8vxL1gOVvj+W7ruEO4s36eCw==} + + synchronous-promise@2.0.17: + resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + sywac@1.3.0: + resolution: {integrity: sha512-LDt2stNTp4bVPMgd70Jj9PWrSa4batl+bv+Ea5NLNGT7ufc4oQPtRfQ73wbddNV6RilaPqnEt6y1Wkm5FVTNEg==} + engines: {node: '>=4'} + + tabbable@5.3.3: + resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-api-utils@1.0.3: + resolution: {integrity: sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ==} + peerDependencies: + tailwindcss: ^3.3.0 || ^4.0.0 || ^4.0.0-beta + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@3.4.18: + resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tap-parser@7.0.0: + resolution: {integrity: sha512-05G8/LrzqOOFvZhhAk32wsGiPZ1lfUrl+iV7+OkKgfofZxiceZWMHkKmow71YsyVQ8IvGBP2EjcIjE5gL4l5lA==} + hasBin: true + + tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.8: + resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} + engines: {node: '>=18'} + + tarn@2.0.0: + resolution: {integrity: sha512-7rNMCZd3s9bhQh47ksAQd92ADFcJUjjbyOvyFjNLwTPpGieFHMC84S+LOzw0fx1uh6hnDz/19r8CPMnIjJlMMA==} + engines: {node: '>=8.0.0'} + + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + telejson@7.2.0: + resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + terser-webpack-plugin@1.4.6: + resolution: {integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@4.8.1: + resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} + engines: {node: '>=6.0.0'} + hasBin: true + + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + testem@3.19.1: + resolution: {integrity: sha512-h9LKg7pAF3B0aqp3V3Kx8vFlB/ocB1xXvRY1YxZyIKvV/3ZUXqYadi8OD2T15VoFjUZlRrm39s9e7N8A+gYBfA==} + engines: {node: '>= 7.*'} + hasBin: true + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + textextensions@2.6.0: + resolution: {integrity: sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==} + engines: {node: '>=0.8'} + + thenby@1.3.4: + resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@3.0.2: + resolution: {integrity: sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + time-zone@1.0.0: + resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} + engines: {node: '>=4'} + + timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + + timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-lr@2.0.0: + resolution: {integrity: sha512-f6nh0VMRvhGx4KCeK1lQ/jaL0Zdb5WdR+Jk8q9OSUQnaSDxAEGH1fgqLZ+cMl5EW3F2MGnCsalBO1IsnnogW1Q==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} + hasBin: true + + tldts-core@7.0.27: + resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==} + + tldts@7.0.27: + resolution: {integrity: sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==} + hasBin: true + + tmp@0.0.28: + resolution: {integrity: sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg==} + engines: {node: '>=0.4.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.1.0: + resolution: {integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==} + engines: {node: '>=6'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + to-fast-properties@1.0.3: + resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} + engines: {node: '>=0.10.0'} + + to-iso-string@0.0.2: + resolution: {integrity: sha512-oeHLgfWA7d0CPQa6h0+i5DAJZISz5un0d5SHPkw+Untclcvzv9T+AC3CvGXlZJdOlIbxbTfyyzlqCXc5hjpXYg==} + deprecated: to-iso-string has been deprecated, use @segment/to-iso-string instead. + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + to-through@3.0.0: + resolution: {integrity: sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==} + engines: {node: '>=10.13.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + tooltip.js@1.3.3: + resolution: {integrity: sha512-XWWuy/dBdF/F/YpRE955yqBZ4VdLfiTAUdOqoU+wJm6phJlMpEzl/iYHZ+qJswbeT9VG822bNfsETF9wzmoy5A==} + deprecated: Tooltip.js is not supported anymore, please migrate to tippy.js + + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tracked-built-ins@3.4.0: + resolution: {integrity: sha512-aRwWQXC3VkY50oYxS7wKZiavkjf3uaN+UYUH30D5gxUqbxDN2LnNsfWyDfckmxHUGw4gJDH5lpRS0jX/tim0vw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tree-sync@1.4.0: + resolution: {integrity: sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ==} + + tree-sync@2.1.0: + resolution: {integrity: sha512-OLWW+Nd99NOM53aZ8ilT/YpEiOo6mXD3F4/wLbARqybSZ3Jb8IxHK5UGVbZaae0wtXAyQshVV+SeqVBik+Fbmw==} + engines: {node: '>=8'} + + trim-newlines@4.1.1: + resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} + engines: {node: '>=12'} + + trim-right@1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + + trim-trailing-lines@1.1.4: + resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==} + + trim@0.0.1: + resolution: {integrity: sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==} + deprecated: Use String.prototype.trim() instead + + trough@1.0.5: + resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.4.9: + resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + tty-browserify@0.0.0: + resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.11.0: + resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} + engines: {node: '>=8'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.5.0: + resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@1.0.4: + resolution: {integrity: sha512-vjMKrfSoUDN8/Vnqitw2FmstOfuJ73G6CrSEKnf11A6RmasVxHqfeBcnTb6RsL4pTMuV5Zsv9IiHRphMZyckUw==} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript-memoize@1.1.1: + resolution: {integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + underscore.string@3.3.6: + resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} + + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + + underscore@1.7.0: + resolution: {integrity: sha512-cp0oQQyZhUM1kpJDLdGO1jPZHgS/MpzoWYfe9+CM2h/QGDZlqwT2T3YGukuBdaNJ/CAPoeyAZRRHz8JFo176vA==} + + underscore@1.8.3: + resolution: {integrity: sha512-5WsVTFcH1ut/kkhAaHf4PVgI8c7++GiVcpCGxPouI6ZVjsqPnSDf8h/8HtVqc0t4fzRXwnMK70EcZeAs3PIddg==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + undici-types@7.24.7: + resolution: {integrity: sha512-XA+gOBkzYD3C74sZowtCLTpgtaCdqZhqCvR6y9LXvrKTt/IVU6bz49T4D+BPi475scshCCkb0IklJRw6T1ZlgQ==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + undici@6.24.1: + resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} + engines: {node: '>=18.17'} + + undici@7.24.6: + resolution: {integrity: sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==} + engines: {node: '>=20.18.1'} + + unherit@1.1.3: + resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unidecode@0.1.8: + resolution: {integrity: sha512-SdoZNxCWpN2tXTCrGkPF/0rL2HEq+i2gwRG1ReBvx8/0yTzC3enHfugOf8A9JBShVwwrRIkLX0YcDUGbzjbVCA==} + engines: {node: '>= 0.4.12'} + + unidecode@1.1.0: + resolution: {integrity: sha512-GIp57N6DVVJi8dpeIU6/leJGdv7W65ZSXFLFiNmxvexXkc0nXdqUvhA/qL9KqBKsILxMwg5MnmYNOIDJLb5JVA==} + engines: {node: '>= 0.4.12'} + + unified@8.4.2: + resolution: {integrity: sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA==} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + + uniqs@2.0.0: + resolution: {integrity: sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + unist-util-is@3.0.0: + resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-remove-position@1.1.4: + resolution: {integrity: sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-visit-parents@2.1.2: + resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit@1.4.1: + resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@1.16.1: + resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} + engines: {node: '>=14.0.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + unquote@1.1.1: + resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + untildify@2.1.0: + resolution: {integrity: sha512-sJjbDp2GodvkB0FZZcn7k6afVisqX5BZD7Yq3xp4nN2O15BBK0cLm3Vwn2vQaF7UDS0UUsrQMkkplmDI5fskig==} + engines: {node: '>=0.10.0'} + + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url-regex-safe@4.0.0: + resolution: {integrity: sha512-BrnFCWKNFrFnRzKD66NtJqQepfJrUHNPvPxE5y5NSAhXBb4OlobQjt7907Jm4ItPiXaeX+dDWMkcnOd4jR9N8A==} + engines: {node: '>= 14'} + peerDependencies: + re2: ^1.20.1 + peerDependenciesMeta: + re2: + optional: true + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-debounce@10.1.1: + resolution: {integrity: sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '*' + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-resize-observer@9.1.0: + resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} + peerDependencies: + react: 16.8.0 - 18 + react-dom: 16.8.0 - 18 + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + username-sync@1.0.3: + resolution: {integrity: sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util.promisify@1.0.1: + resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + util@0.11.1: + resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + uuid@7.0.3: + resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + v8flags@3.2.0: + resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==} + engines: {node: '>= 0.10'} + + valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@3.0.0: + resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} + + validate-peer-dependencies@1.2.0: + resolution: {integrity: sha512-nd2HUpKc6RWblPZQ2GDuI65sxJ2n/UqZwSBVtj64xlWjMx0m7ZB2m9b2JS3v1f+n9VWH/dd1CMhkHfP6pIdckA==} + + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + validator@7.2.0: + resolution: {integrity: sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg==} + engines: {node: '>= 0.10'} + + value-or-function@4.0.0: + resolution: {integrity: sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==} + engines: {node: '>= 10.13.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + velocity-animate@1.5.2: + resolution: {integrity: sha512-m6EXlCAMetKztO1ppBhGU1/1MR3IiEevO6ESq6rcrSQ3Q77xYSW13jkfXW88o4xMrkXJhy/U7j4wFR/twMB0Eg==} + + vendors@1.0.4: + resolution: {integrity: sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vfile-location@2.0.6: + resolution: {integrity: sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + video-extensions@1.2.0: + resolution: {integrity: sha512-TriMl18BHEsh2KuuSA065tbu4SNAC9fge7k8uKoTTofTq89+Xsg4K1BGbmSVETwUZhqSjd9KwRCNwXAW/buXMg==} + engines: {node: '>=0.10.0'} + + vinyl-contents@2.0.0: + resolution: {integrity: sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==} + engines: {node: '>=10.13.0'} + + vinyl-fs@4.0.2: + resolution: {integrity: sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==} + engines: {node: '>=10.13.0'} + + vinyl-sourcemap@2.0.0: + resolution: {integrity: sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==} + engines: {node: '>=10.13.0'} + + vinyl@3.0.1: + resolution: {integrity: sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==} + engines: {node: '>=10.13.0'} + + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-css-injected-by-js@3.5.2: + resolution: {integrity: sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==} + peerDependencies: + vite: '>2.0.0-0' + + vite-plugin-svgr@3.3.0: + resolution: {integrity: sha512-vWZMCcGNdPqgziYFKQ3Y95XP0d0YGp28+MM3Dp9cTa/px5CKcHHrIoPl2Jw81rgVm6/ZUNONzjXbZQZ7Kw66og==} + peerDependencies: + vite: ^2.6.0 || 3 || 4 + + vite-plugin-svgr@4.5.0: + resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==} + peerDependencies: + vite: '>=2.6.0' + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.1.12: + resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + + vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + walk-sync@0.3.4: + resolution: {integrity: sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig==} + + walk-sync@1.1.4: + resolution: {integrity: sha512-nowc9thB/Jg0KW4TgxoRjLLYRPvl3DB/98S89r4ZcJqq2B0alNcKDh6pzLkBSkPMzRSMsJghJHQi79qw0YWEkA==} + + walk-sync@2.2.0: + resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} + engines: {node: 8.* || >= 10.*} + + walk-sync@3.0.0: + resolution: {integrity: sha512-41TvKmDGVpm2iuH7o+DAOt06yyu/cSHpX3uzAwetzASvlNtVddgIjXIb2DfB/Wa20B1Jo86+1Dv1CraSU7hWdw==} + engines: {node: 10.* || >= 12.*} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watch-detector@1.0.2: + resolution: {integrity: sha512-MrJK9z7kD5Gl3jHBnnBVHvr1saVGAfmkyyrvuNzV/oe0Gr1nwZTy5VSA0Gw2j2Or0Mu8HcjUa44qlBvC2Ofnpg==} + engines: {node: '>= 8'} + + watchpack-chokidar2@2.0.1: + resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==} + + watchpack@1.7.5: + resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-resource-inliner@6.0.1: + resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} + engines: {node: '>=10.0.0'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-sources@1.4.3: + resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} + + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@4.47.0: + resolution: {integrity: sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==} + engines: {node: '>=6.11.5'} + hasBin: true + peerDependencies: + webpack-cli: '*' + webpack-command: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + webpack-command: + optional: true + + webpack@5.105.4: + resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + whoops@4.1.8: + resolution: {integrity: sha512-HuSP8Fm1phqNtzyWxiQgt9MivugPlIO2oFGk1qUzga9Yjrr+Yt5C3r3UFgOECgh9LZo04lsNBecZXpfAMvbBzQ==} + engines: {node: '>= 8'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@0.0.3: + resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==} + engines: {node: '>=0.4.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + worker-farm@1.7.0: + resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==} + + workerpool@2.3.4: + resolution: {integrity: sha512-c2EWrgB9IKHi1jbf4LG9sxKgHYOY+Ej5li6siEGtFecCXWG7eQOqATPEJ0rg1KFETXROEkErc1t5XiNrLG666Q==} + + workerpool@3.1.2: + resolution: {integrity: sha512-WJFA0dGqIK7qj7xPTqciWBH5DlJQzoPjsANvc3Y4hNB0SScT+Emjvt0jPPkDBUjBNngX1q9hHgt1Gfwytu6pug==} + + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + + workerpool@9.3.4: + resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==} + + world-countries@5.1.0: + resolution: {integrity: sha512-CXR6EBvTbArDlDDIWU3gfKb7Qk0ck2WNZ234b/A0vuecPzIfzzxH+O6Ejnvg1sT8XuiZjVlzOH0h08ZtaO7g0w==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrap-legacy-hbs-plugin-if-needed@1.0.1: + resolution: {integrity: sha512-aJjXe5WwrY0u0dcUgKW3m2SGnxosJ66LLm/QaG0YMHqgA6+J2xwAFZfhSLsQ2BmO5x8PTH+OIxoAXuGz3qBA7A==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + 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 + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xregexp@2.0.0: + resolution: {integrity: sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==} + + xtend@2.0.6: + resolution: {integrity: sha512-fOZg4ECOlrMl+A6Msr7EIFcON1L26mb4NY5rurSkOex/TWhazOrg6eXD/B0XkuiYcYhQDWLXzQxLMVJ7LXwokg==} + engines: {node: '>=0.4'} + + xtend@2.1.2: + resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==} + engines: {node: '>=0.4'} + + xtend@2.2.0: + resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==} + engines: {node: '>=0.4'} + + xtend@3.0.0: + resolution: {integrity: sha512-sp/sT9OALMjRW1fKDlPeuSZlDQpkqReA0pyJukniWbTGoEKefHxhGJynE3PNhUMlcM8qWIjPwecwCw4LArS5Eg==} + engines: {node: '>=0.4'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yam@1.0.0: + resolution: {integrity: sha512-Hv9xxHtsJ9228wNhk03xnlDReUuWVvHwM4rIbjdAXYvHLs17xjuyF50N6XXFMN6N0omBaqgOok/MCK3At9fTAg==} + engines: {node: ^4.5 || 6.* || >= 7.*} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + yup@0.32.9: + resolution: {integrity: sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==} + engines: {node: '>=10'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@actions/core@3.0.0': + dependencies: + '@actions/exec': 3.0.0 + '@actions/http-client': 4.0.0 + + '@actions/exec@3.0.0': + dependencies: + '@actions/io': 3.0.2 + + '@actions/http-client@4.0.0': + dependencies: + tunnel: 0.0.6 + undici: 6.24.1 + + '@actions/io@3.0.2': {} + + '@adobe/css-tools@4.4.4': {} + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.7 + + '@asamuzakjp/css-color@5.0.1': + dependencies: + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.7 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.7 + + '@asamuzakjp/dom-selector@7.0.4': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.7 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.6 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1025.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-node': 3.972.29 + '@aws-sdk/middleware-bucket-endpoint': 3.972.8 + '@aws-sdk/middleware-expect-continue': 3.972.8 + '@aws-sdk/middleware-flexible-checksums': 3.974.6 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-location-constraint': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-sdk-s3': 3.972.27 + '@aws-sdk/middleware-ssec': 3.972.8 + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/region-config-resolver': 3.972.10 + '@aws-sdk/signature-v4-multi-region': 3.996.15 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.14 + '@smithy/config-resolver': 4.4.13 + '@smithy/core': 3.23.13 + '@smithy/eventstream-serde-browser': 4.2.12 + '@smithy/eventstream-serde-config-resolver': 4.3.12 + '@smithy/eventstream-serde-node': 4.2.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-blob-browser': 4.2.13 + '@smithy/hash-node': 4.2.12 + '@smithy/hash-stream-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/md5-js': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.1 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.13 + '@smithy/util-stream': 4.5.21 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.14 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-ses@3.1018.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.25 + '@aws-sdk/credential-provider-node': 3.972.26 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.26 + '@aws-sdk/region-config-resolver': 3.972.10 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.12 + '@smithy/config-resolver': 4.4.13 + '@smithy/core': 3.23.12 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.27 + '@smithy/middleware-retry': 4.4.44 + '@smithy/middleware-serde': 4.2.15 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.0 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.7 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.43 + '@smithy/util-defaults-mode-node': 4.2.47 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.13 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.25': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/xml-builder': 3.972.16 + '@smithy/core': 3.23.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.7 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.973.26': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/xml-builder': 3.972.16 + '@smithy/core': 3.23.13 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.5': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.23': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.24': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.1 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.21 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.26': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.1 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.21 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-env': 3.972.23 + '@aws-sdk/credential-provider-http': 3.972.25 + '@aws-sdk/credential-provider-login': 3.972.25 + '@aws-sdk/credential-provider-process': 3.972.23 + '@aws-sdk/credential-provider-sso': 3.972.25 + '@aws-sdk/credential-provider-web-identity': 3.972.25 + '@aws-sdk/nested-clients': 3.996.15 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-ini@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-login': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.15 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.26': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.23 + '@aws-sdk/credential-provider-http': 3.972.25 + '@aws-sdk/credential-provider-ini': 3.972.25 + '@aws-sdk/credential-provider-process': 3.972.23 + '@aws-sdk/credential-provider-sso': 3.972.25 + '@aws-sdk/credential-provider-web-identity': 3.972.25 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.29': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.24 + '@aws-sdk/credential-provider-http': 3.972.26 + '@aws-sdk/credential-provider-ini': 3.972.28 + '@aws-sdk/credential-provider-process': 3.972.24 + '@aws-sdk/credential-provider-sso': 3.972.28 + '@aws-sdk/credential-provider-web-identity': 3.972.28 + '@aws-sdk/types': 3.973.6 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.23': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.24': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.15 + '@aws-sdk/token-providers': 3.1018.0 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-sso@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/token-providers': 3.1021.0 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.15 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.6': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/crc64-nvme': 3.972.5 + '@aws-sdk/types': 3.973.6 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.21 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.6 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.27': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.13 + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.21 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.26': + dependencies: + '@aws-sdk/core': 3.973.25 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-retry': 4.2.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@smithy/core': 3.23.13 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-retry': 4.2.13 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.15': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/region-config-resolver': 3.972.10 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.14 + '@smithy/config-resolver': 4.4.13 + '@smithy/core': 3.23.13 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.1 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/nested-clients@3.996.18': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.26 + '@aws-sdk/middleware-host-header': 3.972.8 + '@aws-sdk/middleware-logger': 3.972.8 + '@aws-sdk/middleware-recursion-detection': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/region-config-resolver': 3.972.10 + '@aws-sdk/types': 3.973.6 + '@aws-sdk/util-endpoints': 3.996.5 + '@aws-sdk/util-user-agent-browser': 3.972.8 + '@aws-sdk/util-user-agent-node': 3.973.14 + '@smithy/config-resolver': 4.4.13 + '@smithy/core': 3.23.13 + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/hash-node': 4.2.12 + '@smithy/invalid-dependency': 4.2.12 + '@smithy/middleware-content-length': 4.2.12 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-retry': 4.4.46 + '@smithy/middleware-serde': 4.2.16 + '@smithy/middleware-stack': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/node-http-handler': 4.5.1 + '@smithy/protocol-http': 5.3.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.44 + '@smithy/util-defaults-mode-node': 4.2.48 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/config-resolver': 4.4.13 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.15': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.27 + '@aws-sdk/types': 3.973.6 + '@smithy/protocol-http': 5.3.12 + '@smithy/signature-v4': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1018.0': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.15 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.1021.0': + dependencies: + '@aws-sdk/core': 3.973.26 + '@aws-sdk/nested-clients': 3.996.18 + '@aws-sdk/types': 3.973.6 + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.6': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.5': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-endpoints': 3.3.3 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.8': + dependencies: + '@aws-sdk/types': 3.973.6 + '@smithy/types': 4.13.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.12': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.26 + '@aws-sdk/types': 3.973.6 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.14': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.28 + '@aws-sdk/types': 3.973.6 + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.16': + dependencies: + '@smithy/types': 4.13.1 + fast-xml-parser: 5.5.8 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/eslint-parser@7.28.4(@babel/core@7.29.0)(eslint@8.57.1)': + dependencies: + '@babel/core': 7.29.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-assign@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.4.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + + '@babel/plugin-transform-typescript@7.5.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.8.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/polyfill@7.12.1': + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.13.11 + + '@babel/preset-env@7.29.2(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + esutils: 2.0.3 + + '@babel/runtime@7.12.18': + dependencies: + regenerator-runtime: 0.13.11 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@balena/dockerignore@1.0.2': {} + + '@bcoe/v8-coverage@0.2.3': {} + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@breejs/later@4.2.0': {} + + '@cnakazawa/watch@1.0.4': + dependencies: + exec-sh: 0.3.6 + minimist: 1.2.8 + + '@codemirror/autocomplete@6.20.1': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/common': 1.5.1 + + '@codemirror/commands@6.10.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/common': 1.5.1 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.3 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.3 + '@lezer/html': 1.3.13 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.5 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/common': 1.5.1 + '@lezer/javascript': 1.5.4 + + '@codemirror/language@6.12.3': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.5': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + crelt: 1.0.6 + + '@codemirror/search@6.6.0': + dependencies: + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + crelt: 1.0.6 + + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.40.0': + dependencies: + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/convert-colors@1.4.0': {} + + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@2.4.1': {} + + '@csstools/css-tokenizer@4.0.0': {} + + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + + '@csstools/postcss-cascade-layers@1.1.1(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-color-function@1.1.1(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-font-format-keywords@1.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-hwb-function@1.0.2(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-ic-unit@1.0.1(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + '@csstools/postcss-nested-calc@1.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@1.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@1.1.1(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-unset-value@1.0.2(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@csstools/selector-specificity@3.1.1(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@distributed-systems/callsite@1.1.1': + dependencies: + ee-log: 3.0.9 + section-tests: 1.3.1 + + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@doist/react-interpolate@2.2.1(prop-types@15.8.1)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + prop-types: 15.8.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + + '@ebay/nice-modal-react@1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@elastic/elasticsearch@8.13.1': + dependencies: + '@elastic/transport': 8.4.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@elastic/transport@8.4.1': + dependencies: + debug: 4.4.3(supports-color@5.5.0) + hpagent: 1.2.0 + ms: 2.1.3 + secure-json-parse: 2.7.0 + tslib: 2.8.1 + undici: 5.29.0 + transitivePeerDependencies: + - supports-color + + '@ember-data/adapter@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember-data/store': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@ember/string': 1.1.0 + ember-cli-babel: 7.26.11 + ember-cli-test-info: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/canary-features@3.24.0(@babel/core@7.29.0)': + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/debug@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@ember/string': 1.1.0 + ember-cli-babel: 7.26.11 + ember-cli-test-info: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/model@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/canary-features': 3.24.0(@babel/core@7.29.0) + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember-data/store': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@ember/string': 1.1.0 + ember-cli-babel: 7.26.11 + ember-cli-string-utils: 1.1.0 + ember-cli-test-info: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + inflection: 1.12.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/private-build-infra@3.24.0(@babel/core@7.29.0)': + dependencies: + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@ember-data/canary-features': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-filter-imports: 4.0.0 + babel6-plugin-strip-class-callcheck: 6.0.0 + broccoli-debug: 0.6.5 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 4.2.0 + broccoli-rollup: 4.1.1 + calculate-cache-key-for-tree: 2.0.0 + chalk: 4.1.2 + ember-cli-babel: 7.26.11 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + ember-cli-version-checker: 5.1.2 + esm: 3.2.25 + git-repo-info: 2.1.1 + glob: 7.2.3 + npm-git-info: 1.0.3 + rimraf: 3.0.2 + rsvp: 4.8.5 + semver: 7.7.4 + silent-error: 1.1.1 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/record-data@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/canary-features': 3.24.0(@babel/core@7.29.0) + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember-data/store': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@ember/ordered-set': 4.0.0(@babel/core@7.29.0) + ember-cli-babel: 7.26.11 + ember-cli-test-info: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/rfc395-data@0.0.4': {} + + '@ember-data/serializer@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember-data/store': 3.24.0(@babel/core@7.29.0) + ember-cli-babel: 7.26.11 + ember-cli-test-info: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-data/store@3.24.0(@babel/core@7.29.0)': + dependencies: + '@ember-data/canary-features': 3.24.0(@babel/core@7.29.0) + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember/string': 1.1.0 + ember-cli-babel: 7.26.11 + ember-cli-path-utils: 1.0.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + heimdalljs: 0.3.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember-decorators/component@6.1.1': + dependencies: + '@ember-decorators/utils': 6.1.1 + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + '@ember-decorators/object@6.1.1': + dependencies: + '@ember-decorators/utils': 6.1.1 + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + '@ember-decorators/utils@6.1.1': + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + '@ember/edition-utils@1.2.0': {} + + '@ember/jquery@2.0.0': + dependencies: + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + ember-cli-babel: 7.26.11 + jquery: 3.7.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@ember/optional-features@2.1.0': + dependencies: + chalk: 4.1.2 + ember-cli-version-checker: 5.1.2 + glob: 7.2.3 + inquirer: 7.3.3 + mkdirp: 1.0.4 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@ember/ordered-set@4.0.0(@babel/core@7.29.0)': + dependencies: + ember-cli-babel: 7.26.11 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember/render-modifiers@2.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))': + dependencies: + '@embroider/macros': 1.16.13 + ember-cli-babel: 7.26.11 + ember-modifier-manager-polyfill: 1.2.0(@babel/core@7.29.0) + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember/string@1.1.0': + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + '@ember/string@3.1.1': + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + '@ember/test-helpers@1.7.3(@babel/core@7.29.0)': + dependencies: + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + ember-assign-polyfill: 2.7.3(@babel/core@7.29.0) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars-inline-precompile: 2.1.0(ember-cli-babel@7.26.11) + ember-test-waiters: 1.2.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@ember/test-helpers@2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))': + dependencies: + '@ember/test-waiters': 3.1.0 + '@embroider/macros': 1.16.13 + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-destroyable-polyfill: 2.0.3(@babel/core@7.29.0) + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - supports-color + + '@ember/test-waiters@3.1.0': + dependencies: + calculate-cache-key-for-tree: 2.0.0 + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@embroider/addon-shim@1.10.2': + dependencies: + '@embroider/shared-internals': 3.0.2 + broccoli-funnel: 3.0.8 + common-ancestor-path: 1.0.1 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@embroider/core@0.29.0': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@embroider/macros': 0.29.0 + assert-never: 1.4.0 + babel-plugin-syntax-dynamic-import: 6.18.0 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 3.1.0 + broccoli-source: 1.1.0 + debug: 3.2.7 + fast-sourcemap-concat: 1.4.0 + filesize: 4.2.1 + fs-extra: 7.0.1 + fs-tree-diff: 2.0.1 + handlebars: 4.7.9 + js-string-escape: 1.0.1 + jsdom: 16.7.0 + json-stable-stringify: 1.3.0 + lodash: 4.17.23 + pkg-up: 2.0.0 + resolve: 1.22.11 + resolve-package-path: 1.2.7 + semver: 7.7.4 + strip-bom: 3.0.0 + typescript-memoize: 1.1.1 + walk-sync: 1.1.4 + wrap-legacy-hbs-plugin-if-needed: 1.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@embroider/macros@0.29.0': + dependencies: + '@babel/core': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@embroider/core': 0.29.0 + assert-never: 1.4.0 + ember-cli-babel: 7.26.11 + lodash: 4.17.23 + resolve: 1.22.11 + semver: 7.7.4 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@embroider/macros@0.41.0': + dependencies: + '@embroider/shared-internals': 0.41.0 + assert-never: 1.4.0 + ember-cli-babel: 7.26.11 + lodash: 4.17.23 + resolve: 1.22.11 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@embroider/macros@1.16.13': + dependencies: + '@embroider/shared-internals': 2.9.0 + assert-never: 1.4.0 + babel-import-util: 2.1.1 + ember-cli-babel: 7.26.11 + find-up: 5.0.0 + lodash: 4.17.23 + resolve: 1.22.11 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@embroider/macros@1.20.2': + dependencies: + '@embroider/shared-internals': 3.0.2 + assert-never: 1.4.0 + babel-import-util: 3.0.1 + ember-cli-babel: 7.26.11 + find-up: 5.0.0 + lodash: 4.17.23 + resolve: 1.22.11 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@embroider/shared-internals@0.41.0': + dependencies: + ember-rfc176-data: 0.3.18 + fs-extra: 7.0.1 + lodash: 4.17.23 + pkg-up: 3.1.0 + resolve-package-path: 1.2.7 + semver: 7.7.4 + typescript-memoize: 1.1.1 + + '@embroider/shared-internals@1.8.3': + dependencies: + babel-import-util: 1.4.1 + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + resolve-package-path: 4.0.3 + semver: 7.7.4 + typescript-memoize: 1.1.1 + + '@embroider/shared-internals@2.9.0': + dependencies: + babel-import-util: 2.1.1 + debug: 4.4.3(supports-color@5.5.0) + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + minimatch: 3.1.5 + pkg-entry-points: 1.1.1 + resolve-package-path: 4.0.3 + semver: 7.7.4 + typescript-memoize: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@embroider/shared-internals@2.9.2': + dependencies: + babel-import-util: 2.1.1 + debug: 4.4.3(supports-color@5.5.0) + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + minimatch: 3.1.5 + pkg-entry-points: 1.1.1 + resolve-package-path: 4.0.3 + semver: 7.7.4 + typescript-memoize: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@embroider/shared-internals@3.0.2': + dependencies: + babel-import-util: 3.0.1 + debug: 4.4.3(supports-color@5.5.0) + ember-rfc176-data: 0.3.18 + fs-extra: 9.1.0 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + minimatch: 3.1.5 + pkg-entry-points: 1.1.1 + resolve-package-path: 4.0.3 + resolve.exports: 2.0.3 + semver: 7.7.4 + typescript-memoize: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@embroider/util@1.13.5(ember-source@3.24.0(@babel/core@7.29.0))': + dependencies: + '@embroider/macros': 1.20.2 + broccoli-funnel: 3.0.8 + ember-cli-babel: 7.26.11 + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.20.2': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.20.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.20.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.20.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.20.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.20.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.20.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.20.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.20.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.20.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.20.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.20.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.20.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.20.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.20.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.20.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.20.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.20.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.20.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.20.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.20.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.20.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.20.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.37.0(jiti@2.6.1))': + dependencies: + eslint: 9.37.0(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.16.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.14.0 + debug: 4.4.3(supports-color@5.5.0) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3(supports-color@5.5.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@eslint/js@9.37.0': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@exodus/bytes@1.15.0(@noble/hashes@1.8.0)': + optionalDependencies: + '@noble/hashes': 1.8.0 + + '@extractus/oembed-extractor@3.2.1(encoding@0.1.13)': + dependencies: + cross-fetch: 4.1.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@faker-js/faker@7.6.0': {} + + '@faker-js/faker@8.4.1': {} + + '@faker-js/faker@9.9.0': {} + + '@fastify/busboy@2.1.1': {} + + '@fastify/otel@0.18.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.11': {} + + '@gar/promise-retry@1.0.3': {} + + '@gar/promisify@1.1.3': + optional: true + + '@glimmer/component@1.1.2(@babel/core@7.29.0)': + dependencies: + '@glimmer/di': 0.1.11 + '@glimmer/env': 0.1.7 + '@glimmer/util': 0.44.0 + broccoli-file-creator: 2.1.1 + broccoli-merge-trees: 3.0.2 + ember-cli-babel: 7.26.11 + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 3.0.0(@babel/core@7.29.0) + ember-cli-version-checker: 3.1.3 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@glimmer/di@0.1.11': {} + + '@glimmer/encoder@0.42.2': + dependencies: + '@glimmer/interfaces': 0.42.2 + '@glimmer/vm': 0.42.2 + + '@glimmer/env@0.1.7': {} + + '@glimmer/global-context@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + + '@glimmer/interfaces@0.42.2': {} + + '@glimmer/interfaces@0.84.3': + dependencies: + '@simple-dom/interface': 1.4.0 + + '@glimmer/interfaces@0.94.6': + dependencies: + '@simple-dom/interface': 1.4.0 + type-fest: 4.41.0 + + '@glimmer/low-level@0.42.2': {} + + '@glimmer/program@0.42.2': + dependencies: + '@glimmer/encoder': 0.42.2 + '@glimmer/interfaces': 0.42.2 + '@glimmer/util': 0.42.2 + + '@glimmer/reference@0.42.2': + dependencies: + '@glimmer/util': 0.42.2 + + '@glimmer/reference@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.84.3 + '@glimmer/interfaces': 0.84.3 + '@glimmer/util': 0.84.3 + '@glimmer/validator': 0.84.3 + + '@glimmer/runtime@0.42.2': + dependencies: + '@glimmer/interfaces': 0.42.2 + '@glimmer/low-level': 0.42.2 + '@glimmer/program': 0.42.2 + '@glimmer/reference': 0.42.2 + '@glimmer/util': 0.42.2 + '@glimmer/vm': 0.42.2 + '@glimmer/wire-format': 0.42.2 + + '@glimmer/syntax@0.42.2': + dependencies: + '@glimmer/interfaces': 0.42.2 + '@glimmer/util': 0.42.2 + handlebars: 4.7.9 + simple-html-tokenizer: 0.5.11 + + '@glimmer/syntax@0.84.3': + dependencies: + '@glimmer/interfaces': 0.84.3 + '@glimmer/util': 0.84.3 + '@handlebars/parser': 2.0.0 + simple-html-tokenizer: 0.5.11 + + '@glimmer/syntax@0.95.0': + dependencies: + '@glimmer/interfaces': 0.94.6 + '@glimmer/util': 0.94.8 + '@glimmer/wire-format': 0.94.8 + '@handlebars/parser': 2.2.2 + simple-html-tokenizer: 0.5.11 + + '@glimmer/tracking@1.1.2': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/validator': 0.44.0 + + '@glimmer/util@0.42.2': {} + + '@glimmer/util@0.44.0': {} + + '@glimmer/util@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.84.3 + '@simple-dom/interface': 1.4.0 + + '@glimmer/util@0.94.8': + dependencies: + '@glimmer/interfaces': 0.94.6 + + '@glimmer/validator@0.44.0': {} + + '@glimmer/validator@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.84.3 + + '@glimmer/vm@0.42.2': + dependencies: + '@glimmer/interfaces': 0.42.2 + '@glimmer/util': 0.42.2 + + '@glimmer/wire-format@0.42.2': + dependencies: + '@glimmer/interfaces': 0.42.2 + '@glimmer/util': 0.42.2 + + '@glimmer/wire-format@0.94.8': + dependencies: + '@glimmer/interfaces': 0.94.6 + + '@grpc/grpc-js@1.14.3': + dependencies: + '@grpc/proto-loader': 0.8.0 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@gulpjs/to-absolute-glob@4.0.0': + dependencies: + is-negated-glob: 1.0.0 + + '@handlebars/parser@2.0.0': {} + + '@handlebars/parser@2.2.2': {} + + '@headlessui/react@1.7.19(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@tanstack/react-virtual': 3.13.12(react-dom@17.0.2(react@17.0.2))(react@17.0.2) + client-only: 0.0.1 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + + '@hookform/resolvers@5.2.2(react-hook-form@7.72.1(react@18.3.1))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.72.1(react@18.3.1) + + '@html-next/vertical-collection@3.0.0(@babel/core@7.29.0)': + dependencies: + babel6-plugin-strip-class-callcheck: 6.0.0 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + broccoli-rollup: 4.1.1 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-cli-version-checker: 3.1.3 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + ember-raf-scheduler: 0.3.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@html-validate/stylish@4.3.0': + dependencies: + kleur: 4.1.5 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@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.9.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 + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@25.6.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.6.0) + '@inquirer/type': 3.0.10(@types/node@25.6.0) + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/core@10.3.2(@types/node@25.6.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.6.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/external-editor@1.0.3(@types/node@22.19.17)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.19.17 + + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.6.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@25.6.0)': + optionalDependencies: + '@types/node': 25.6.0 + + '@ioredis/commands@1.5.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@isaacs/ttlcache@1.4.1': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(node-notifier@10.0.1) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + optional: true + + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3))': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0(node-notifier@10.0.1) + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/diff-sequences@30.3.0': {} + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + jest-mock: 29.7.0 + + '@jest/expect-utils@28.1.3': + dependencies: + jest-get-type: 28.0.2 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@28.1.3': + dependencies: + expect: 28.1.3 + jest-snapshot: 28.1.3 + transitivePeerDependencies: + - supports-color + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 25.6.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0(node-notifier@10.0.1)': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 25.6.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@28.1.3': + dependencies: + '@sinclair/typebox': 0.24.51 + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.48 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@28.1.3': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 28.1.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@28.1.3': + dependencies: + '@jest/schemas': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 25.6.0 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + glob: 10.5.0 + magic-string: 0.27.0 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + optionalDependencies: + typescript: 5.9.3 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + glob: 13.0.6 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + optionalDependencies: + typescript: 5.9.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@juggle/resize-observer@3.4.0': {} + + '@kapouer/eslint-plugin-no-return-in-loop@1.0.0': {} + + '@keyv/serialize@1.1.1': {} + + '@keyvhq/core@2.1.15': + dependencies: + json-buffer: 3.0.1 + + '@keyvhq/memoize@2.0.3': + dependencies: + '@keyvhq/core': 2.1.15 + mimic-fn: 3.0.0 + + '@kikobeats/time-span@1.0.12': {} + + '@lexical/clipboard@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/html': 0.13.1(lexical@0.13.1) + '@lexical/list': 0.13.1(lexical@0.13.1) + '@lexical/selection': 0.13.1(lexical@0.13.1) + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/code@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + prismjs: 1.30.0 + + '@lexical/headless@0.13.1(lexical@0.13.1)': + dependencies: + lexical: 0.13.1 + + '@lexical/html@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/selection': 0.13.1(lexical@0.13.1) + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/link@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/list@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/rich-text@0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1)': + dependencies: + '@lexical/clipboard': 0.13.1(lexical@0.13.1) + '@lexical/selection': 0.13.1(lexical@0.13.1) + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/selection@0.13.1(lexical@0.13.1)': + dependencies: + lexical: 0.13.1 + + '@lexical/table@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/utils': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lexical/utils@0.13.1(lexical@0.13.1)': + dependencies: + '@lexical/list': 0.13.1(lexical@0.13.1) + '@lexical/selection': 0.13.1(lexical@0.13.1) + '@lexical/table': 0.13.1(lexical@0.13.1) + lexical: 0.13.1 + + '@lezer/common@1.5.1': {} + + '@lezer/css@1.3.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.1 + + '@lint-todo/utils@13.1.1': + dependencies: + '@types/eslint': 8.56.12 + find-up: 5.0.0 + fs-extra: 9.1.0 + proper-lockfile: 4.1.2 + slash: 3.0.0 + tslib: 2.8.1 + upath: 2.0.1 + + '@marijn/find-cluster-break@1.0.2': {} + + '@mdx-js/react@3.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 18.3.28 + react: 18.3.1 + + '@metascraper/helpers@5.50.0(@noble/hashes@1.8.0)': + dependencies: + audio-extensions: 0.0.0 + chrono-node: 2.9.0 + condense-whitespace: 2.0.0 + data-uri-utils: 1.0.12 + debug-logfmt: 1.4.10 + entities: 7.0.1 + file-extension: 4.0.5 + has-values: 2.0.1 + image-extensions: 1.1.0 + is-relative-url: 3.0.0 + is-uri: 1.2.13 + iso-639-3: 2.2.0 + isostring: 0.0.1 + jsdom: 27.4.0(@noble/hashes@1.8.0) + jsonrepair: 3.13.3 + lodash: 4.17.23 + memoize-one: 6.0.0 + microsoft-capitalize: 1.0.7 + mime: 3.0.0 + normalize-url: 6.1.0 + re2: 1.23.3 + smartquotes: 2.3.2 + tldts: 7.0.27 + url-regex-safe: 4.0.0(re2@1.23.3) + video-extensions: 1.2.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@miragejs/pretender-node-polyfill@0.1.2': {} + + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.9.0 + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + dependencies: + eslint-scope: 5.1.1 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@npmcli/agent@4.0.0': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.2.7 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.4 + optional: true + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.7.4 + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@npmcli/redact@4.0.0': {} + + '@number-flow/react@0.5.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + esm-env: 1.2.2 + number-flow: 0.5.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@nx/nx-darwin-arm64@22.0.4': + optional: true + + '@nx/nx-darwin-x64@22.0.4': + optional: true + + '@nx/nx-freebsd-x64@22.0.4': + optional: true + + '@nx/nx-linux-arm-gnueabihf@22.0.4': + optional: true + + '@nx/nx-linux-arm64-gnu@22.0.4': + optional: true + + '@nx/nx-linux-arm64-musl@22.0.4': + optional: true + + '@nx/nx-linux-x64-gnu@22.0.4': + optional: true + + '@nx/nx-linux-x64-musl@22.0.4': + optional: true + + '@nx/nx-win32-arm64-msvc@22.0.4': + optional: true + + '@nx/nx-win32-x64-msvc@22.0.4': + optional: true + + '@one-ini/wasm@0.1.1': + optional: true + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@opentelemetry/api-logs@0.207.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation-amqplib@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.31.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.23.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.67.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.24.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.207.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.38.2': {} + + '@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@otplib/preset-default@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@otplib/preset-v11@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 + + '@polka/url@1.0.0-next.29': {} + + '@popperjs/core@2.11.8': {} + + '@prettier/sync@0.6.1(prettier@2.8.8)': + dependencies: + make-synchronized: 0.8.0 + prettier: 2.8.8 + + '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@radix-ui/number@1.0.1': + dependencies: + '@babel/runtime': 7.29.2 + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.29.2 + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-avatar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.1.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-direction@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-form@0.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-label': 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-id@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popper@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-select@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-slot@1.0.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-slot@1.2.4(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-previous@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/rect': 1.0.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-size@1.0.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/rect@1.0.1': + dependencies: + '@babel/runtime': 7.29.2 + + '@radix-ui/rect@1.1.1': {} + + '@remirror/core-constants@3.0.0': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rolldown/pluginutils@1.0.0-beta.35': {} + + '@rollup/pluginutils@5.3.0(rollup@4.60.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.0 + + '@rollup/rollup-android-arm-eabi@4.60.0': + optional: true + + '@rollup/rollup-android-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-x64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@selderee/plugin-htmlparser2@0.6.0': + dependencies: + domhandler: 4.3.1 + selderee: 0.6.0 + + '@sentry-internal/feedback@7.120.3': + dependencies: + '@sentry/core': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry-internal/feedback@7.120.4': + dependencies: + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry-internal/replay-canvas@7.120.3': + dependencies: + '@sentry/core': 7.120.3 + '@sentry/replay': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry-internal/replay-canvas@7.120.4': + dependencies: + '@sentry/core': 7.120.4 + '@sentry/replay': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry-internal/tracing@7.116.0': + dependencies: + '@sentry/core': 7.116.0 + '@sentry/types': 7.116.0 + '@sentry/utils': 7.116.0 + + '@sentry-internal/tracing@7.120.3': + dependencies: + '@sentry/core': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry-internal/tracing@7.120.4': + dependencies: + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry/browser@7.120.3': + dependencies: + '@sentry-internal/feedback': 7.120.3 + '@sentry-internal/replay-canvas': 7.120.3 + '@sentry-internal/tracing': 7.120.3 + '@sentry/core': 7.120.3 + '@sentry/integrations': 7.120.3 + '@sentry/replay': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry/browser@7.120.4': + dependencies: + '@sentry-internal/feedback': 7.120.4 + '@sentry-internal/replay-canvas': 7.120.4 + '@sentry-internal/tracing': 7.120.4 + '@sentry/core': 7.120.4 + '@sentry/integrations': 7.120.4 + '@sentry/replay': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry/core@10.47.0': {} + + '@sentry/core@7.114.0': + dependencies: + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + + '@sentry/core@7.116.0': + dependencies: + '@sentry/types': 7.116.0 + '@sentry/utils': 7.116.0 + + '@sentry/core@7.120.3': + dependencies: + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry/core@7.120.4': + dependencies: + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry/ember@7.120.3(webpack@5.105.4(@swc/core@1.15.21))': + dependencies: + '@embroider/macros': 1.16.13 + '@sentry/browser': 7.120.3 + '@sentry/core': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + ember-auto-import: 2.10.0(webpack@5.105.4(@swc/core@1.15.21)) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-cli-typescript: 5.3.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + - webpack + + '@sentry/integrations@7.114.0': + dependencies: + '@sentry/core': 7.114.0 + '@sentry/types': 7.114.0 + '@sentry/utils': 7.114.0 + localforage: 1.10.0 + + '@sentry/integrations@7.120.3': + dependencies: + '@sentry/core': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + localforage: 1.10.0 + + '@sentry/integrations@7.120.4': + dependencies: + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + localforage: 1.10.0 + + '@sentry/node-core@10.47.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@sentry/core': 10.47.0 + '@sentry/opentelemetry': 10.47.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@sentry/node@10.47.0': + dependencies: + '@fastify/otel': 0.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.31.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-express': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.23.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.67.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.24.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@prisma/instrumentation': 7.6.0(@opentelemetry/api@1.9.1) + '@sentry/core': 10.47.0 + '@sentry/node-core': 10.47.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/opentelemetry': 10.47.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/node@7.120.4': + dependencies: + '@sentry-internal/tracing': 7.120.4 + '@sentry/core': 7.120.4 + '@sentry/integrations': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry/opentelemetry@10.47.0(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.6.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@sentry/core': 10.47.0 + + '@sentry/react@7.120.4(react@17.0.2)': + dependencies: + '@sentry/browser': 7.120.4 + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + hoist-non-react-statics: 3.3.2 + react: 17.0.2 + + '@sentry/react@7.120.4(react@18.3.1)': + dependencies: + '@sentry/browser': 7.120.4 + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + + '@sentry/replay@7.116.0': + dependencies: + '@sentry-internal/tracing': 7.116.0 + '@sentry/core': 7.116.0 + '@sentry/types': 7.116.0 + '@sentry/utils': 7.116.0 + + '@sentry/replay@7.120.3': + dependencies: + '@sentry-internal/tracing': 7.120.3 + '@sentry/core': 7.120.3 + '@sentry/types': 7.120.3 + '@sentry/utils': 7.120.3 + + '@sentry/replay@7.120.4': + dependencies: + '@sentry-internal/tracing': 7.120.4 + '@sentry/core': 7.120.4 + '@sentry/types': 7.120.4 + '@sentry/utils': 7.120.4 + + '@sentry/types@7.114.0': {} + + '@sentry/types@7.116.0': {} + + '@sentry/types@7.120.3': {} + + '@sentry/types@7.120.4': {} + + '@sentry/utils@7.114.0': + dependencies: + '@sentry/types': 7.114.0 + + '@sentry/utils@7.116.0': + dependencies: + '@sentry/types': 7.116.0 + + '@sentry/utils@7.120.3': + dependencies: + '@sentry/types': 7.120.3 + + '@sentry/utils@7.120.4': + dependencies: + '@sentry/types': 7.120.4 + + '@sidvind/better-ajv-errors@3.0.1(ajv@8.18.0)': + dependencies: + ajv: 8.18.0 + kleur: 4.1.5 + + '@simple-dom/document@1.4.0': + dependencies: + '@simple-dom/interface': 1.4.0 + + '@simple-dom/interface@1.4.0': {} + + '@simple-dom/parser@1.4.0': + dependencies: + '@simple-dom/interface': 1.4.0 + + '@simple-dom/serializer@1.4.0': + dependencies: + '@simple-dom/interface': 1.4.0 + + '@simple-dom/void-map@1.4.0': {} + + '@sinclair/typebox@0.24.51': {} + + '@sinclair/typebox@0.27.10': {} + + '@sinclair/typebox@0.34.48': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/is@5.6.0': {} + + '@sindresorhus/is@7.2.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sinonjs/commons@1.8.6': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@11.2.2': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@15.1.1': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@15.3.1': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@sinonjs/fake-timers@6.0.1': + dependencies: + '@sinonjs/commons': 1.8.6 + + '@sinonjs/samsam@10.0.1': + dependencies: + '@sinonjs/commons': 3.0.1 + type-detect: 4.1.0 + + '@sinonjs/samsam@5.3.1': + dependencies: + '@sinonjs/commons': 1.8.6 + lodash.get: 4.4.2 + type-detect: 4.1.0 + + '@sinonjs/samsam@8.0.3': + dependencies: + '@sinonjs/commons': 3.0.1 + type-detect: 4.1.0 + + '@sinonjs/text-encoding@0.7.3': {} + + '@slack/types@2.20.1': {} + + '@slack/webhook@7.0.9': + dependencies: + '@slack/types': 2.20.1 + '@types/node': 25.6.0 + axios: 1.15.0 + transitivePeerDependencies: + - debug + + '@smithy/abort-controller@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.2.3': + dependencies: + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.13': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.3 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/core@3.23.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.20 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/core@3.23.13': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-stream': 4.5.21 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.12': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.12': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.12': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.12': + dependencies: + '@smithy/eventstream-codec': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.15': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.13': + dependencies: + '@smithy/chunked-blob-reader': 5.2.2 + '@smithy/chunked-blob-reader-native': 4.2.3 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.12': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.27': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-serde': 4.2.15 + '@smithy/node-config-provider': 4.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.28': + dependencies: + '@smithy/core': 3.23.13 + '@smithy/middleware-serde': 4.2.16 + '@smithy/node-config-provider': 4.3.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + '@smithy/url-parser': 4.2.12 + '@smithy/util-middleware': 4.2.12 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.44': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/service-error-classification': 4.2.12 + '@smithy/smithy-client': 4.12.7 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.12 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.4.46': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/service-error-classification': 4.2.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-retry': 4.2.13 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.15': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.16': + dependencies: + '@smithy/core': 3.23.13 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.12': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/shared-ini-file-loader': 4.4.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.0': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.1': + dependencies: + '@smithy/protocol-http': 5.3.12 + '@smithy/querystring-builder': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + + '@smithy/shared-ini-file-loader@4.4.7': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.12': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.12 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.7': + dependencies: + '@smithy/core': 3.23.12 + '@smithy/middleware-endpoint': 4.4.27 + '@smithy/middleware-stack': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.20 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.8': + dependencies: + '@smithy/core': 3.23.13 + '@smithy/middleware-endpoint': 4.4.28 + '@smithy/middleware-stack': 4.2.12 + '@smithy/protocol-http': 5.3.12 + '@smithy/types': 4.13.1 + '@smithy/util-stream': 4.5.21 + tslib: 2.8.1 + + '@smithy/types@4.13.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.12': + dependencies: + '@smithy/querystring-parser': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.43': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.44': + dependencies: + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.47': + dependencies: + '@smithy/config-resolver': 4.4.13 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.7 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.48': + dependencies: + '@smithy/config-resolver': 4.4.13 + '@smithy/credential-provider-imds': 4.2.12 + '@smithy/node-config-provider': 4.3.12 + '@smithy/property-provider': 4.2.12 + '@smithy/smithy-client': 4.12.8 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.3': + dependencies: + '@smithy/node-config-provider': 4.3.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.12': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.12': + dependencies: + '@smithy/service-error-classification': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.2.13': + dependencies: + '@smithy/service-error-classification': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.20': + dependencies: + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.1 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.21': + dependencies: + '@smithy/fetch-http-handler': 5.3.15 + '@smithy/node-http-handler': 4.5.1 + '@smithy/types': 4.13.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.13': + dependencies: + '@smithy/abort-controller': 4.2.12 + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.14': + dependencies: + '@smithy/types': 4.13.1 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@socket.io/component-emitter@3.1.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@stdlib/array-float32@0.2.3': + dependencies: + '@stdlib/assert-has-float32array-support': 0.2.3 + + '@stdlib/array-float64@0.2.3': + dependencies: + '@stdlib/assert-has-float64array-support': 0.2.3 + + '@stdlib/array-int16@0.2.3': + dependencies: + '@stdlib/assert-has-int16array-support': 0.2.3 + + '@stdlib/array-int32@0.2.3': + dependencies: + '@stdlib/assert-has-int32array-support': 0.2.3 + + '@stdlib/array-int8@0.2.3': + dependencies: + '@stdlib/assert-has-int8array-support': 0.2.3 + + '@stdlib/array-uint16@0.2.3': + dependencies: + '@stdlib/assert-has-uint16array-support': 0.2.3 + + '@stdlib/array-uint32@0.2.3': + dependencies: + '@stdlib/assert-has-uint32array-support': 0.2.3 + + '@stdlib/array-uint8@0.2.3': + dependencies: + '@stdlib/assert-has-uint8array-support': 0.2.3 + + '@stdlib/array-uint8c@0.2.3': + dependencies: + '@stdlib/assert-has-uint8clampedarray-support': 0.2.3 + + '@stdlib/assert-has-float32array-support@0.2.3': + dependencies: + '@stdlib/assert-is-float32array': 0.2.3 + '@stdlib/constants-float64-pinf': 0.2.3 + + '@stdlib/assert-has-float64array-support@0.2.3': + dependencies: + '@stdlib/assert-is-float64array': 0.2.3 + + '@stdlib/assert-has-int16array-support@0.2.3': + dependencies: + '@stdlib/assert-is-int16array': 0.2.3 + '@stdlib/constants-int16-max': 0.2.3 + '@stdlib/constants-int16-min': 0.2.3 + + '@stdlib/assert-has-int32array-support@0.2.3': + dependencies: + '@stdlib/assert-is-int32array': 0.2.3 + '@stdlib/constants-int32-max': 0.3.1 + '@stdlib/constants-int32-min': 0.2.3 + + '@stdlib/assert-has-int8array-support@0.2.3': + dependencies: + '@stdlib/assert-is-int8array': 0.2.3 + '@stdlib/constants-int8-max': 0.2.3 + '@stdlib/constants-int8-min': 0.2.3 + + '@stdlib/assert-has-node-buffer-support@0.2.3': + dependencies: + '@stdlib/assert-is-buffer': 0.2.3 + + '@stdlib/assert-has-own-property@0.2.3': {} + + '@stdlib/assert-has-symbol-support@0.2.3': {} + + '@stdlib/assert-has-to-primitive-symbol-support@0.1.1': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/symbol-ctor': 0.2.3 + + '@stdlib/assert-has-tostringtag-support@0.2.3': + dependencies: + '@stdlib/assert-has-symbol-support': 0.2.3 + + '@stdlib/assert-has-uint16array-support@0.2.3': + dependencies: + '@stdlib/assert-is-uint16array': 0.2.3 + '@stdlib/constants-uint16-max': 0.2.3 + + '@stdlib/assert-has-uint32array-support@0.2.3': + dependencies: + '@stdlib/assert-is-uint32array': 0.2.3 + '@stdlib/constants-uint32-max': 0.2.3 + + '@stdlib/assert-has-uint8array-support@0.2.3': + dependencies: + '@stdlib/assert-is-uint8array': 0.2.3 + '@stdlib/constants-uint8-max': 0.2.3 + + '@stdlib/assert-has-uint8clampedarray-support@0.2.3': + dependencies: + '@stdlib/assert-is-uint8clampedarray': 0.2.3 + + '@stdlib/assert-is-arguments@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-is-array': 0.2.3 + '@stdlib/assert-is-enumerable-property': 0.2.3 + '@stdlib/constants-uint32-max': 0.2.3 + '@stdlib/math-base-assert-is-integer': 0.2.7 + '@stdlib/utils-native-class': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-big-endian@0.2.3': + dependencies: + '@stdlib/array-uint16': 0.2.3 + '@stdlib/array-uint8': 0.2.3 + + '@stdlib/assert-is-boolean@0.2.3': + dependencies: + '@stdlib/assert-has-tostringtag-support': 0.2.3 + '@stdlib/boolean-ctor': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-buffer@0.2.3': + dependencies: + '@stdlib/assert-is-object-like': 0.2.3 + + '@stdlib/assert-is-collection@0.2.3': + dependencies: + '@stdlib/constants-array-max-typed-array-length': 0.2.3 + '@stdlib/math-base-assert-is-integer': 0.2.7 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-enumerable-property@0.2.3': + dependencies: + '@stdlib/assert-is-integer': 0.2.3 + '@stdlib/assert-is-nan': 0.2.3 + '@stdlib/assert-is-string': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-error@0.2.3': + dependencies: + '@stdlib/utils-get-prototype-of': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-float32array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-float64array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-function@0.2.3': + dependencies: + '@stdlib/utils-type-of': 0.2.3 + + '@stdlib/assert-is-int16array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-int32array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-int8array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-integer@0.2.3': + dependencies: + '@stdlib/assert-is-number': 0.2.3 + '@stdlib/constants-float64-ninf': 0.2.3 + '@stdlib/constants-float64-pinf': 0.2.3 + '@stdlib/math-base-assert-is-integer': 0.2.7 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-little-endian@0.2.3': + dependencies: + '@stdlib/array-uint16': 0.2.3 + '@stdlib/array-uint8': 0.2.3 + + '@stdlib/assert-is-nan@0.2.3': + dependencies: + '@stdlib/assert-is-number': 0.2.3 + '@stdlib/math-base-assert-is-nan': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-nonnegative-integer@0.2.3': + dependencies: + '@stdlib/assert-is-integer': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-is-number@0.2.3': + dependencies: + '@stdlib/assert-has-tostringtag-support': 0.2.3 + '@stdlib/number-ctor': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-object-like@0.2.3': + dependencies: + '@stdlib/assert-tools-array-function': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/assert-is-object@0.2.3': + dependencies: + '@stdlib/assert-is-array': 0.2.3 + + '@stdlib/assert-is-plain-object@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-is-function': 0.2.3 + '@stdlib/assert-is-object': 0.2.3 + '@stdlib/utils-get-prototype-of': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-regexp@0.2.3': + dependencies: + '@stdlib/assert-has-tostringtag-support': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-string@0.2.3': + dependencies: + '@stdlib/assert-has-tostringtag-support': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-uint16array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-uint32array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-uint8array@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-is-uint8clampedarray@0.2.3': + dependencies: + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/assert-napi-equal-types@0.2.3': + dependencies: + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-napi-is-type@0.2.3': + dependencies: + '@stdlib/assert-napi-equal-types': 0.2.3 + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/assert-napi-status-ok@0.2.3': {} + + '@stdlib/assert-tools-array-function@0.2.3': + dependencies: + '@stdlib/assert-is-array': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/boolean-ctor@0.2.3': {} + + '@stdlib/buffer-ctor@0.2.3': + dependencies: + '@stdlib/assert-has-node-buffer-support': 0.2.3 + + '@stdlib/buffer-from-buffer@0.2.3': + dependencies: + '@stdlib/assert-is-buffer': 0.2.3 + '@stdlib/assert-is-function': 0.2.3 + '@stdlib/buffer-ctor': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/complex-float32-ctor@0.1.1': + dependencies: + '@stdlib/assert-is-number': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/number-float64-base-to-float32': 0.2.3 + '@stdlib/string-format': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-define-read-only-property': 0.2.3 + + '@stdlib/complex-float32-reim@0.1.4': + dependencies: + '@stdlib/array-float32': 0.2.3 + '@stdlib/complex-float32-ctor': 0.1.1 + + '@stdlib/complex-float64-ctor@0.1.2': + dependencies: + '@stdlib/assert-is-number': 0.2.3 + '@stdlib/complex-float32-ctor': 0.1.1 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-define-read-only-property': 0.2.3 + + '@stdlib/complex-float64-reim@0.1.4': + dependencies: + '@stdlib/array-float64': 0.2.3 + '@stdlib/complex-float64-ctor': 0.1.2 + + '@stdlib/constants-array-max-typed-array-length@0.2.3': {} + + '@stdlib/constants-float16-eps@0.2.3': {} + + '@stdlib/constants-float16-exponent-bias@0.3.1': {} + + '@stdlib/constants-float16-exponent-mask@0.1.1': {} + + '@stdlib/constants-float16-max@0.2.3': {} + + '@stdlib/constants-float16-num-significand-bits@0.0.2': {} + + '@stdlib/constants-float16-sign-mask@0.1.1': {} + + '@stdlib/constants-float16-significand-mask@0.1.1': {} + + '@stdlib/constants-float16-smallest-normal@0.2.3': {} + + '@stdlib/constants-float32-abs-mask@0.2.3': {} + + '@stdlib/constants-float32-eps@0.2.3': + dependencies: + '@stdlib/number-float64-base-to-float32': 0.2.3 + + '@stdlib/constants-float32-exponent-bias@0.2.3': {} + + '@stdlib/constants-float32-exponent-mask@0.2.3': {} + + '@stdlib/constants-float32-ninf@0.2.3': + dependencies: + '@stdlib/array-float32': 0.2.3 + '@stdlib/array-uint32': 0.2.3 + + '@stdlib/constants-float32-num-significand-bits@0.1.1': {} + + '@stdlib/constants-float32-pinf@0.2.3': + dependencies: + '@stdlib/array-float32': 0.2.3 + '@stdlib/array-uint32': 0.2.3 + + '@stdlib/constants-float32-sign-mask@0.2.3': {} + + '@stdlib/constants-float32-significand-mask@0.2.4': {} + + '@stdlib/constants-float64-eps@0.2.3': {} + + '@stdlib/constants-float64-high-word-abs-mask@0.2.3': {} + + '@stdlib/constants-float64-ninf@0.2.3': + dependencies: + '@stdlib/number-ctor': 0.2.3 + + '@stdlib/constants-float64-pinf@0.2.3': {} + + '@stdlib/constants-int16-max@0.2.3': {} + + '@stdlib/constants-int16-min@0.2.3': {} + + '@stdlib/constants-int32-max@0.3.1': {} + + '@stdlib/constants-int32-min@0.2.3': {} + + '@stdlib/constants-int8-max@0.2.3': {} + + '@stdlib/constants-int8-min@0.2.3': {} + + '@stdlib/constants-uint16-max@0.2.3': {} + + '@stdlib/constants-uint32-max@0.2.3': {} + + '@stdlib/constants-uint8-max@0.2.3': {} + + '@stdlib/error-tools-fmtprodmsg@0.2.3': {} + + '@stdlib/fs-exists@0.2.3': + dependencies: + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/fs-resolve-parent-path@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-is-function': 0.2.3 + '@stdlib/assert-is-plain-object': 0.2.3 + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/fs-exists': 0.2.3 + '@stdlib/process-cwd': 0.2.3 + '@stdlib/string-format': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/math-base-assert-is-finite@0.2.3': + dependencies: + '@stdlib/constants-float64-ninf': 0.2.3 + '@stdlib/constants-float64-pinf': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/napi-argv-double': 0.2.2 + '@stdlib/napi-create-int32': 0.0.3 + '@stdlib/napi-export': 0.3.1 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-assert-is-finitef@0.2.3': + dependencies: + '@stdlib/constants-float32-ninf': 0.2.3 + '@stdlib/constants-float32-pinf': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/napi-argv-float': 0.2.3 + '@stdlib/napi-create-int32': 0.0.3 + '@stdlib/napi-export': 0.3.1 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-assert-is-integer@0.2.7': + dependencies: + '@stdlib/math-base-special-floor': 0.2.4 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/napi-argv-double': 0.2.2 + '@stdlib/napi-create-int32': 0.0.3 + '@stdlib/napi-export': 0.3.1 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-assert-is-nan@0.2.3': + dependencies: + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-assert-is-nanf@0.2.3': + dependencies: + '@stdlib/napi-argv': 0.2.3 + '@stdlib/napi-argv-float': 0.2.3 + '@stdlib/napi-create-int32': 0.0.3 + '@stdlib/napi-export': 0.3.1 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-napi-unary@0.2.7': + dependencies: + '@stdlib/complex-float32-ctor': 0.1.1 + '@stdlib/complex-float32-reim': 0.1.4 + '@stdlib/complex-float64-ctor': 0.1.2 + '@stdlib/complex-float64-reim': 0.1.4 + '@stdlib/number-float16-base-to-float64': 0.1.2 + '@stdlib/number-float16-ctor': 0.1.2 + '@stdlib/number-float64-base-to-float16': 0.1.2 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-special-abs@0.2.3': + dependencies: + '@stdlib/constants-float64-high-word-abs-mask': 0.2.3 + '@stdlib/math-base-napi-unary': 0.2.7 + '@stdlib/number-float64-base-to-words': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-special-absf@0.2.3': + dependencies: + '@stdlib/constants-float32-abs-mask': 0.2.3 + '@stdlib/math-base-napi-unary': 0.2.7 + '@stdlib/number-float32-base-to-word': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/math-base-special-floor@0.2.4': + dependencies: + '@stdlib/math-base-napi-unary': 0.2.7 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/napi-argv-double@0.2.2': + dependencies: + '@stdlib/assert-napi-is-type': 0.2.3 + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/napi-argv-float@0.2.3': + dependencies: + '@stdlib/assert-napi-is-type': 0.2.3 + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/napi-argv-int32@0.2.3': + dependencies: + '@stdlib/assert-napi-is-type': 0.2.3 + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/napi-argv@0.2.3': + dependencies: + '@stdlib/assert-napi-status-ok': 0.2.3 + + '@stdlib/napi-create-int32@0.0.3': + dependencies: + '@stdlib/assert-napi-status-ok': 0.2.3 + '@stdlib/napi-argv': 0.2.3 + '@stdlib/napi-argv-int32': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/napi-export@0.3.1': {} + + '@stdlib/number-ctor@0.2.3': {} + + '@stdlib/number-float16-base-to-float32@0.1.2': + dependencies: + '@stdlib/constants-float16-exponent-bias': 0.3.1 + '@stdlib/constants-float16-exponent-mask': 0.1.1 + '@stdlib/constants-float16-num-significand-bits': 0.0.2 + '@stdlib/constants-float16-sign-mask': 0.1.1 + '@stdlib/constants-float16-significand-mask': 0.1.1 + '@stdlib/constants-float32-exponent-bias': 0.2.3 + '@stdlib/constants-float32-exponent-mask': 0.2.3 + '@stdlib/constants-float32-num-significand-bits': 0.1.1 + '@stdlib/number-float16-ctor': 0.1.2 + '@stdlib/number-float32-base-to-float16': 0.1.1 + '@stdlib/number-float64-base-to-float16': 0.1.2 + '@stdlib/number-float64-base-to-float32': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float16-base-to-float64@0.1.2': + dependencies: + '@stdlib/number-float16-base-to-float32': 0.1.2 + '@stdlib/number-float16-ctor': 0.1.2 + '@stdlib/number-float64-base-to-float16': 0.1.2 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float16-ctor@0.1.2': + dependencies: + '@stdlib/assert-has-to-primitive-symbol-support': 0.1.1 + '@stdlib/assert-is-number': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/number-float64-base-to-float16': 0.1.2 + '@stdlib/string-format': 0.2.3 + '@stdlib/symbol-to-primitive': 0.1.2 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-define-read-only-property': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float32-base-exponent@0.2.4': + dependencies: + '@stdlib/constants-float32-exponent-bias': 0.2.3 + '@stdlib/constants-float32-exponent-mask': 0.2.3 + '@stdlib/number-float32-base-to-word': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float32-base-to-float16@0.1.1': + dependencies: + '@stdlib/constants-float16-eps': 0.2.3 + '@stdlib/constants-float16-max': 0.2.3 + '@stdlib/constants-float16-smallest-normal': 0.2.3 + '@stdlib/constants-float32-eps': 0.2.3 + '@stdlib/constants-float32-exponent-mask': 0.2.3 + '@stdlib/constants-float32-num-significand-bits': 0.1.1 + '@stdlib/constants-float32-pinf': 0.2.3 + '@stdlib/constants-float32-sign-mask': 0.2.3 + '@stdlib/constants-float32-significand-mask': 0.2.4 + '@stdlib/math-base-assert-is-finitef': 0.2.3 + '@stdlib/math-base-assert-is-nanf': 0.2.3 + '@stdlib/math-base-special-absf': 0.2.3 + '@stdlib/number-float16-ctor': 0.1.2 + '@stdlib/number-float32-base-exponent': 0.2.4 + '@stdlib/number-float64-base-to-float32': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float32-base-to-word@0.2.3': + dependencies: + '@stdlib/array-float32': 0.2.3 + '@stdlib/array-uint32': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float64-base-to-float16@0.1.2': + dependencies: + '@stdlib/constants-float16-eps': 0.2.3 + '@stdlib/constants-float16-max': 0.2.3 + '@stdlib/constants-float16-smallest-normal': 0.2.3 + '@stdlib/constants-float64-eps': 0.2.3 + '@stdlib/constants-float64-pinf': 0.2.3 + '@stdlib/math-base-assert-is-finite': 0.2.3 + '@stdlib/math-base-special-abs': 0.2.3 + '@stdlib/number-float16-ctor': 0.1.2 + '@stdlib/number-float32-base-to-float16': 0.1.1 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/number-float64-base-to-float32@0.2.3': + dependencies: + '@stdlib/array-float32': 0.2.3 + + '@stdlib/number-float64-base-to-words@0.2.3': + dependencies: + '@stdlib/array-float64': 0.2.3 + '@stdlib/array-uint32': 0.2.3 + '@stdlib/assert-is-little-endian': 0.2.3 + '@stdlib/os-byte-order': 0.2.3 + '@stdlib/os-float-word-order': 0.2.3 + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + '@stdlib/utils-library-manifest': 0.2.4 + transitivePeerDependencies: + - supports-color + + '@stdlib/object-ctor@0.2.2': {} + + '@stdlib/os-byte-order@0.2.3': + dependencies: + '@stdlib/assert-is-big-endian': 0.2.3 + '@stdlib/assert-is-little-endian': 0.2.3 + + '@stdlib/os-float-word-order@0.2.3': + dependencies: + '@stdlib/os-byte-order': 0.2.3 + + '@stdlib/process-cwd@0.2.3': {} + + '@stdlib/regexp-extended-length-path@0.2.3': + dependencies: + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/regexp-function-name@0.2.3': + dependencies: + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/regexp-regexp@0.2.3': + dependencies: + '@stdlib/utils-define-nonenumerable-read-only-property': 0.2.3 + + '@stdlib/string-base-format-interpolate@0.2.4': {} + + '@stdlib/string-base-format-tokenize@0.2.4': {} + + '@stdlib/string-base-lowercase@0.4.1': {} + + '@stdlib/string-base-replace@0.2.3': {} + + '@stdlib/string-format@0.2.3': + dependencies: + '@stdlib/string-base-format-interpolate': 0.2.4 + '@stdlib/string-base-format-tokenize': 0.2.4 + + '@stdlib/string-replace@0.2.3': + dependencies: + '@stdlib/assert-is-function': 0.2.3 + '@stdlib/assert-is-regexp': 0.2.3 + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-base-replace': 0.2.3 + '@stdlib/string-format': 0.2.3 + '@stdlib/utils-escape-regexp-string': 0.2.3 + + '@stdlib/symbol-ctor@0.2.3': {} + + '@stdlib/symbol-to-primitive@0.1.2': + dependencies: + '@stdlib/assert-has-to-primitive-symbol-support': 0.1.1 + '@stdlib/symbol-ctor': 0.2.3 + + '@stdlib/types@0.4.3': {} + + '@stdlib/utils-constructor-name@0.2.3': + dependencies: + '@stdlib/assert-is-buffer': 0.2.3 + '@stdlib/regexp-function-name': 0.2.3 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/utils-convert-path@0.2.3': + dependencies: + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/regexp-extended-length-path': 0.2.3 + '@stdlib/string-base-lowercase': 0.4.1 + '@stdlib/string-format': 0.2.3 + '@stdlib/string-replace': 0.2.3 + + '@stdlib/utils-copy@0.2.3': + dependencies: + '@stdlib/array-float32': 0.2.3 + '@stdlib/array-float64': 0.2.3 + '@stdlib/array-int16': 0.2.3 + '@stdlib/array-int32': 0.2.3 + '@stdlib/array-int8': 0.2.3 + '@stdlib/array-uint16': 0.2.3 + '@stdlib/array-uint32': 0.2.3 + '@stdlib/array-uint8': 0.2.3 + '@stdlib/array-uint8c': 0.2.3 + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-is-array': 0.2.3 + '@stdlib/assert-is-buffer': 0.2.3 + '@stdlib/assert-is-error': 0.2.3 + '@stdlib/assert-is-nonnegative-integer': 0.2.3 + '@stdlib/buffer-from-buffer': 0.2.3 + '@stdlib/constants-float64-pinf': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + '@stdlib/utils-define-property': 0.2.5 + '@stdlib/utils-get-prototype-of': 0.2.3 + '@stdlib/utils-index-of': 0.2.3 + '@stdlib/utils-keys': 0.2.3 + '@stdlib/utils-property-descriptor': 0.2.3 + '@stdlib/utils-property-names': 0.2.3 + '@stdlib/utils-regexp-from-string': 0.2.3 + '@stdlib/utils-type-of': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/utils-define-nonenumerable-read-only-property@0.2.3': + dependencies: + '@stdlib/types': 0.4.3 + '@stdlib/utils-define-property': 0.2.5 + + '@stdlib/utils-define-property@0.2.5': + dependencies: + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/utils-define-read-only-property@0.2.3': + dependencies: + '@stdlib/utils-define-property': 0.2.5 + + '@stdlib/utils-escape-regexp-string@0.2.3': + dependencies: + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/utils-get-prototype-of@0.2.3': + dependencies: + '@stdlib/assert-is-function': 0.2.3 + '@stdlib/object-ctor': 0.2.2 + '@stdlib/utils-native-class': 0.2.3 + + '@stdlib/utils-global@0.2.3': + dependencies: + '@stdlib/assert-is-boolean': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/utils-index-of@0.2.3': + dependencies: + '@stdlib/assert-is-collection': 0.2.3 + '@stdlib/assert-is-integer': 0.2.3 + '@stdlib/assert-is-nan': 0.2.3 + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/string-format': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/utils-keys@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-is-arguments': 0.2.3 + '@stdlib/assert-is-enumerable-property': 0.2.3 + '@stdlib/assert-is-object-like': 0.2.3 + '@stdlib/utils-index-of': 0.2.3 + '@stdlib/utils-noop': 0.2.3 + '@stdlib/utils-type-of': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/utils-library-manifest@0.2.4': + dependencies: + '@stdlib/fs-resolve-parent-path': 0.2.3 + '@stdlib/utils-convert-path': 0.2.3 + debug: 2.6.9 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + '@stdlib/utils-native-class@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + '@stdlib/assert-has-tostringtag-support': 0.2.3 + '@stdlib/symbol-ctor': 0.2.3 + + '@stdlib/utils-noop@0.2.3': {} + + '@stdlib/utils-property-descriptor@0.2.3': + dependencies: + '@stdlib/assert-has-own-property': 0.2.3 + + '@stdlib/utils-property-names@0.2.3': + dependencies: + '@stdlib/object-ctor': 0.2.2 + '@stdlib/utils-keys': 0.2.3 + transitivePeerDependencies: + - supports-color + + '@stdlib/utils-regexp-from-string@0.2.3': + dependencies: + '@stdlib/assert-is-string': 0.2.3 + '@stdlib/error-tools-fmtprodmsg': 0.2.3 + '@stdlib/regexp-regexp': 0.2.3 + '@stdlib/string-format': 0.2.3 + + '@stdlib/utils-type-of@0.2.3': + dependencies: + '@stdlib/utils-constructor-name': 0.2.3 + '@stdlib/utils-global': 0.2.3 + + '@storybook/addon-actions@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + '@types/uuid': 9.0.8 + dequal: 2.0.3 + polished: 4.3.1 + storybook: 8.6.15(prettier@2.8.8) + uuid: 9.0.1 + + '@storybook/addon-backgrounds@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + + '@storybook/addon-controls@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + dequal: 2.0.3 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + + '@storybook/addon-docs@10.3.3(@types/react@18.3.28)(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@18.3.28)(react@18.3.1) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 10.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - esbuild + - rollup + - vite + - webpack + + '@storybook/addon-docs@8.6.14(@types/react@18.3.28)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@18.3.28)(react@18.3.1) + '@storybook/blocks': 8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/csf-plugin': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/react-dom-shim': 8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-essentials@8.6.14(@types/react@18.3.28)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/addon-actions': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-backgrounds': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-controls': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-docs': 8.6.14(@types/react@18.3.28)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-highlight': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-measure': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-outline': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-toolbars': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/addon-viewport': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + + '@storybook/addon-highlight@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/addon-interactions@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/test': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + polished: 4.3.1 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + + '@storybook/addon-links@10.3.3(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + react: 18.3.1 + + '@storybook/addon-links@8.6.14(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + optionalDependencies: + react: 18.3.1 + + '@storybook/addon-measure@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.6.15(prettier@2.8.8) + tiny-invariant: 1.3.3 + + '@storybook/addon-outline@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + + '@storybook/addon-styling@1.3.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(encoding@0.1.13)(less@4.6.4)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12))': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@storybook/api': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 7.6.24(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/core-common': 7.6.24(encoding@0.1.13) + '@storybook/core-events': 7.6.24 + '@storybook/manager-api': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/node-logger': 7.6.24 + '@storybook/preview-api': 7.6.24 + '@storybook/theming': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.24 + css-loader: 6.11.0(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + less-loader: 11.1.4(less@4.6.4)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + prettier: 2.8.8 + resolve-url-loader: 5.0.0 + sass-loader: 13.3.3(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + style-loader: 3.3.4(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + optionalDependencies: + less: 4.6.4 + postcss: 8.5.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - '@types/react-dom' + - encoding + - fibers + - node-sass + - sass + - sass-embedded + - supports-color + - typescript + + '@storybook/addon-styling@1.3.7(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(encoding@0.1.13)(less@4.6.4)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21))': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@storybook/api': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 7.6.24(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/core-common': 7.6.24(encoding@0.1.13) + '@storybook/core-events': 7.6.24 + '@storybook/manager-api': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/node-logger': 7.6.24 + '@storybook/preview-api': 7.6.24 + '@storybook/theming': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.24 + css-loader: 6.11.0(webpack@5.105.4(@swc/core@1.15.21)) + less-loader: 11.1.4(less@4.6.4)(webpack@5.105.4(@swc/core@1.15.21)) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)) + prettier: 2.8.8 + resolve-url-loader: 5.0.0 + sass-loader: 13.3.3(webpack@5.105.4(@swc/core@1.15.21)) + style-loader: 3.3.4(webpack@5.105.4(@swc/core@1.15.21)) + optionalDependencies: + less: 4.6.4 + postcss: 8.5.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + webpack: 5.105.4(@swc/core@1.15.21) + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - '@types/react-dom' + - encoding + - fibers + - node-sass + - sass + - sass-embedded + - supports-color + - typescript + + '@storybook/addon-toolbars@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/addon-viewport@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + memoizerific: 1.11.3 + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/manager-api': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/blocks@8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/icons': 1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/builder-vite@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4))': + dependencies: + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ts-dedent: 2.2.0 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - esbuild + - rollup + - webpack + + '@storybook/builder-vite@8.6.14(storybook@8.6.15(prettier@2.8.8))(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@storybook/csf-plugin': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + browser-assert: 1.2.1 + storybook: 8.6.15(prettier@2.8.8) + ts-dedent: 2.2.0 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + '@storybook/channels@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/global': 5.0.0 + qs: 6.15.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + + '@storybook/channels@7.6.24': + dependencies: + '@storybook/client-logger': 7.6.24 + '@storybook/core-events': 7.6.24 + '@storybook/global': 5.0.0 + qs: 6.15.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + + '@storybook/client-logger@7.6.17': + dependencies: + '@storybook/global': 5.0.0 + + '@storybook/client-logger@7.6.24': + dependencies: + '@storybook/global': 5.0.0 + + '@storybook/components@7.6.24(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-select': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/client-logger': 7.6.24 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/theming': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.24 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-resize-observer: 9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + util-deprecate: 1.0.2 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + '@storybook/components@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/core-common@7.6.24(encoding@0.1.13)': + dependencies: + '@storybook/core-events': 7.6.24 + '@storybook/node-logger': 7.6.24 + '@storybook/types': 7.6.24 + '@types/find-cache-dir': 3.2.1 + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + '@types/pretty-hrtime': 1.0.3 + chalk: 4.1.2 + esbuild: 0.18.20 + esbuild-register: 3.6.0(esbuild@0.18.20) + file-system-cache: 2.3.0 + find-cache-dir: 3.3.2 + find-up: 5.0.0 + fs-extra: 11.3.4 + glob: 10.5.0 + handlebars: 4.7.9 + lazy-universal-dotenv: 4.0.0 + node-fetch: 2.7.0(encoding@0.1.13) + picomatch: 2.3.2 + pkg-dir: 5.0.0 + pretty-hrtime: 1.0.3 + resolve-from: 5.0.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@storybook/core-events@7.6.17': + dependencies: + ts-dedent: 2.2.0 + + '@storybook/core-events@7.6.24': + dependencies: + ts-dedent: 2.2.0 + + '@storybook/core@8.6.15(prettier@2.8.8)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/theming': 8.6.15(storybook@8.6.15(prettier@2.8.8)) + better-opn: 3.0.2 + browser-assert: 1.2.1 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + jsdoc-type-pratt-parser: 4.8.0 + process: 0.11.10 + recast: 0.23.11 + semver: 7.7.4 + util: 0.12.5 + ws: 8.20.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - bufferutil + - storybook + - supports-color + - utf-8-validate + + '@storybook/csf-plugin@10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4))': + dependencies: + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.27.4 + rollup: 4.60.0 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.27.4) + + '@storybook/csf-plugin@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + unplugin: 1.16.1 + + '@storybook/csf@0.1.13': + dependencies: + type-fest: 2.19.0 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/icons@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/instrumenter@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.9 + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/manager-api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/router': 7.6.17 + '@storybook/theming': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.17 + dequal: 2.0.3 + lodash: 4.17.23 + memoizerific: 1.11.3 + store2: 2.14.4 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/manager-api@7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/channels': 7.6.24 + '@storybook/client-logger': 7.6.24 + '@storybook/core-events': 7.6.24 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/router': 7.6.24 + '@storybook/theming': 7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.24 + dequal: 2.0.3 + lodash: 4.17.23 + memoizerific: 1.11.3 + store2: 2.14.4 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/manager-api@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/node-logger@7.6.24': {} + + '@storybook/preview-api@7.6.24': + dependencies: + '@storybook/channels': 7.6.24 + '@storybook/client-logger': 7.6.24 + '@storybook/core-events': 7.6.24 + '@storybook/csf': 0.1.13 + '@storybook/global': 5.0.0 + '@storybook/types': 7.6.24 + '@types/qs': 6.15.0 + dequal: 2.0.3 + lodash: 4.17.23 + memoizerific: 1.11.3 + qs: 6.15.0 + synchronous-promise: 2.0.17 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + + '@storybook/preview-api@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/react-dom-shim@10.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@storybook/react-dom-shim@8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/react-vite@10.3.3(esbuild@0.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@storybook/builder-vite': 10.3.3(esbuild@0.27.4)(rollup@4.60.0)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + '@storybook/react': 10.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + empathic: 2.0.0 + magic-string: 0.30.21 + react: 18.3.1 + react-docgen: 8.0.3 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.11 + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tsconfig-paths: 4.2.0 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - esbuild + - rollup + - supports-color + - typescript + - webpack + + '@storybook/react-vite@8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.60.0)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@storybook/builder-vite': 8.6.14(storybook@8.6.15(prettier@2.8.8))(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)) + '@storybook/react': 8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3) + find-up: 5.0.0 + magic-string: 0.30.21 + react: 18.3.1 + react-docgen: 7.1.1 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.11 + storybook: 8.6.15(prettier@2.8.8) + tsconfig-paths: 4.2.0 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + optionalDependencies: + '@storybook/test': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + '@storybook/react@10.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 10.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: 18.3.1 + react-docgen: 8.0.3 + react-docgen-typescript: 2.4.0(typescript@5.9.3) + react-dom: 18.3.1(react@18.3.1) + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@storybook/react@8.6.14(@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8))(typescript@5.9.3)': + dependencies: + '@storybook/components': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/global': 5.0.0 + '@storybook/manager-api': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/preview-api': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@storybook/react-dom-shim': 8.6.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.15(prettier@2.8.8)) + '@storybook/theming': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.6.15(prettier@2.8.8) + optionalDependencies: + '@storybook/test': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + typescript: 5.9.3 + + '@storybook/router@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + memoizerific: 1.11.3 + qs: 6.15.0 + + '@storybook/router@7.6.24': + dependencies: + '@storybook/client-logger': 7.6.24 + memoizerific: 1.11.3 + qs: 6.15.0 + + '@storybook/test@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.6.14(storybook@8.6.15(prettier@2.8.8)) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/testing-library@0.2.2': + dependencies: + '@testing-library/dom': 9.3.4 + '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) + ts-dedent: 2.2.0 + + '@storybook/theming@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@storybook/client-logger': 7.6.17 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/theming@7.6.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@storybook/client-logger': 7.6.24 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/theming@8.6.14(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/theming@8.6.15(storybook@8.6.15(prettier@2.8.8))': + dependencies: + storybook: 8.6.15(prettier@2.8.8) + + '@storybook/types@7.6.17': + dependencies: + '@storybook/channels': 7.6.17 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.25 + file-system-cache: 2.3.0 + + '@storybook/types@7.6.24': + dependencies: + '@storybook/channels': 7.6.24 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.25 + file-system-cache: 2.3.0 + + '@svg-maps/world@1.0.1': {} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) + + '@svgr/core@8.1.0(typescript@5.9.3)': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.9.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.29.0 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + dependencies: + '@babel/core': 7.29.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@swc/core-darwin-arm64@1.15.21': + optional: true + + '@swc/core-darwin-x64@1.15.21': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.21': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.21': + optional: true + + '@swc/core-linux-arm64-musl@1.15.21': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.21': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.21': + optional: true + + '@swc/core-linux-x64-gnu@1.15.21': + optional: true + + '@swc/core-linux-x64-musl@1.15.21': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.21': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.21': + optional: true + + '@swc/core-win32-x64-msvc@1.15.21': + optional: true + + '@swc/core@1.15.21': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.21 + '@swc/core-darwin-x64': 1.15.21 + '@swc/core-linux-arm-gnueabihf': 1.15.21 + '@swc/core-linux-arm64-gnu': 1.15.21 + '@swc/core-linux-arm64-musl': 1.15.21 + '@swc/core-linux-ppc64-gnu': 1.15.21 + '@swc/core-linux-s390x-gnu': 1.15.21 + '@swc/core-linux-x64-gnu': 1.15.21 + '@swc/core-linux-x64-musl': 1.15.21 + '@swc/core-win32-arm64-msvc': 1.15.21 + '@swc/core-win32-ia32-msvc': 1.15.21 + '@swc/core-win32-x64-msvc': 1.15.21 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@tailwindcss/line-clamp@0.4.4(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/postcss@4.2.1': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + postcss: 8.5.6 + tailwindcss: 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + '@tailwindcss/vite@4.2.1(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@tanstack/query-core@4.36.1': {} + + '@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-core': 4.36.1 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/react-virtual@3.13.12(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + + '@tanstack/react-virtual@3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.13.12': {} + + '@tanstack/virtual-core@3.13.23': {} + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/dom@9.3.4': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@5.17.0': + dependencies: + '@adobe/css-tools': 4.4.4 + '@babel/runtime': 7.29.2 + '@types/testing-library__jest-dom': 5.14.9 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.16 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/jest-dom@6.5.0': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.23 + redent: 3.0.0 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react-hooks@8.0.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + react-error-boundary: 3.1.4(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + react-dom: 18.3.1(react@18.3.1) + + '@testing-library/react@12.1.5(@types/react@18.3.28)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 8.20.1 + '@types/react-dom': 17.0.26(@types/react@18.3.28) + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + transitivePeerDependencies: + - '@types/react' + + '@testing-library/react@14.3.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 9.3.4 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@9.3.4)': + dependencies: + '@testing-library/dom': 9.3.4 + + '@tinybirdco/charts@0.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + echarts: 5.6.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + swr: 2.4.1(react@18.3.1) + + '@tiptap/core@2.26.3(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-blockquote@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-bubble-menu@2.27.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + tippy.js: 6.3.7 + + '@tiptap/extension-document@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-floating-menu@2.27.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + tippy.js: 6.3.7 + + '@tiptap/extension-hard-break@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-link@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + linkifyjs: 4.3.2 + + '@tiptap/extension-paragraph@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/extension-placeholder@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + + '@tiptap/extension-text@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + + '@tiptap/pm@2.26.3': + dependencies: + prosemirror-changeset: 2.4.0 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.4 + prosemirror-menu: 1.3.0 + prosemirror-model: 1.25.4 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7) + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + '@tiptap/react@2.26.3(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)': + dependencies: + '@tiptap/core': 2.26.3(@tiptap/pm@2.26.3) + '@tiptap/extension-bubble-menu': 2.27.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/extension-floating-menu': 2.27.2(@tiptap/core@2.26.3(@tiptap/pm@2.26.3))(@tiptap/pm@2.26.3) + '@tiptap/pm': 2.26.3 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + use-sync-external-store: 1.6.0(react@17.0.2) + + '@tokenizer/token@0.3.0': {} + + '@tootallnate/once@1.1.2': {} + + '@tryghost/adapter-base-cache@0.1.23': {} + + '@tryghost/admin-api-schema@4.7.2': + dependencies: + '@tryghost/errors': 1.3.13 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/api-framework@1.0.7': + dependencies: + '@tryghost/debug': 0.1.40 + '@tryghost/errors': 1.3.13 + '@tryghost/promise': 0.3.20 + '@tryghost/tpl': 0.1.40 + '@tryghost/validator': 0.2.22 + json-stable-stringify: 1.3.0 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-collision@2.0.3': + dependencies: + '@tryghost/errors': 1.3.13 + lodash: 4.17.23 + moment-timezone: 0.5.45 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-custom-query@2.0.3': {} + + '@tryghost/bookshelf-eager-load@2.0.3': + dependencies: + '@tryghost/debug': 2.0.3 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-filter@2.0.3': + dependencies: + '@tryghost/debug': 2.0.3 + '@tryghost/errors': 1.3.13 + '@tryghost/nql': 0.12.10 + '@tryghost/tpl': 2.0.3 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-has-posts@2.1.0': + dependencies: + '@tryghost/debug': 2.0.3 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-include-count@2.0.3': + dependencies: + '@tryghost/debug': 2.0.3 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-order@2.0.3': + dependencies: + lodash: 4.17.23 + + '@tryghost/bookshelf-pagination@2.0.3': + dependencies: + '@tryghost/errors': 1.3.13 + '@tryghost/tpl': 2.0.3 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-plugins@2.0.3': + dependencies: + '@tryghost/bookshelf-collision': 2.0.3 + '@tryghost/bookshelf-custom-query': 2.0.3 + '@tryghost/bookshelf-eager-load': 2.0.3 + '@tryghost/bookshelf-filter': 2.0.3 + '@tryghost/bookshelf-has-posts': 2.1.0 + '@tryghost/bookshelf-include-count': 2.0.3 + '@tryghost/bookshelf-order': 2.0.3 + '@tryghost/bookshelf-pagination': 2.0.3 + '@tryghost/bookshelf-search': 2.0.3 + '@tryghost/bookshelf-transaction-events': 2.0.3 + transitivePeerDependencies: + - supports-color + + '@tryghost/bookshelf-search@2.0.3': {} + + '@tryghost/bookshelf-transaction-events@2.0.3': {} + + '@tryghost/bunyan-rotating-filestream@0.0.7': + dependencies: + long-timeout: 0.1.1 + + '@tryghost/color-utils@0.2.16': + dependencies: + '@types/color': 4.2.0 + color: 3.2.1 + + '@tryghost/config-url-helpers@1.0.23': {} + + '@tryghost/config@2.0.3': + dependencies: + '@tryghost/root-utils': 2.0.3 + nconf: 0.13.0 + + '@tryghost/content-api@1.12.6': + dependencies: + axios: 1.13.6 + transitivePeerDependencies: + - debug + + '@tryghost/custom-fonts@1.0.8': {} + + '@tryghost/database-info@0.3.22': {} + + '@tryghost/database-info@0.3.35': {} + + '@tryghost/debug@0.1.40': + dependencies: + '@tryghost/root-utils': 0.3.38 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@tryghost/debug@2.0.3': + dependencies: + '@tryghost/root-utils': 2.0.3 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@tryghost/domain-events@1.0.8': {} + + '@tryghost/elasticsearch@3.0.29': + dependencies: + '@elastic/elasticsearch': 8.13.1 + '@tryghost/debug': 0.1.40 + split2: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@tryghost/email-mock-receiver@0.3.16': {} + + '@tryghost/ember-promise-modals@2.0.1(ember-source@3.24.0(@babel/core@7.29.0))(postcss@8.5.6)': + dependencies: + '@ember/test-waiters': 3.1.0 + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-postcss: 6.1.0 + ember-auto-import: 1.12.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + focus-trap: 6.9.4 + postcss-preset-env: 7.8.3(postcss@8.5.6) + transitivePeerDependencies: + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - postcss + - supports-color + - webpack-cli + - webpack-command + + '@tryghost/errors@1.3.13': + dependencies: + '@stdlib/utils-copy': 0.2.3 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@tryghost/express-test@0.15.5': + dependencies: + '@tryghost/jest-snapshot': 0.5.23 + cookiejar: 2.1.4 + form-data: 4.0.5 + mime-types: 3.0.2 + reqresnext: 1.7.0 + transitivePeerDependencies: + - supports-color + + '@tryghost/helpers@1.1.103': + dependencies: + lodash-es: 4.17.23 + + '@tryghost/html-to-mobiledoc@3.2.26(@noble/hashes@1.8.0)': + dependencies: + '@tryghost/kg-parser-plugins': 4.2.25 + '@tryghost/mobiledoc-kit': 0.12.4-ghost.1 + jsdom: 29.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - canvas + optional: true + + '@tryghost/html-to-plaintext@1.0.8': + dependencies: + html-to-text: 8.2.1 + lodash: 4.17.23 + + '@tryghost/http-cache-utils@0.1.25': {} + + '@tryghost/http-stream@0.1.42': + dependencies: + '@tryghost/errors': 1.3.13 + '@tryghost/request': 1.0.17 + transitivePeerDependencies: + - supports-color + + '@tryghost/image-transform@1.4.13': + dependencies: + '@tryghost/errors': 1.3.13 + fs-extra: 11.3.4 + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - supports-color + + '@tryghost/jest-snapshot@0.5.23': + dependencies: + '@jest/expect': 28.1.3 + '@jest/expect-utils': 28.1.3 + '@tryghost/errors': 1.3.13 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@tryghost/job-manager@1.0.9': + dependencies: + '@breejs/later': 4.2.0 + '@tryghost/errors': 1.3.13 + '@tryghost/logging': 2.5.5 + bree: 6.5.0 + cron-validate: 1.4.5 + fastq: 1.20.1 + p-wait-for: 3.2.0 + workerpool: 9.3.4 + transitivePeerDependencies: + - supports-color + + '@tryghost/kg-card-factory@5.1.14': {} + + '@tryghost/kg-clean-basic-html@4.2.23': {} + + '@tryghost/kg-converters@1.1.21': + dependencies: + lodash: 4.18.1 + + '@tryghost/kg-default-atoms@5.1.9': {} + + '@tryghost/kg-default-cards@10.2.13(encoding@0.1.13)': + dependencies: + '@tryghost/kg-markdown-html-renderer': 7.1.18 + '@tryghost/kg-utils': 1.0.43 + '@tryghost/string': 0.3.2 + '@tryghost/url-utils': 5.2.2 + handlebars: 4.7.9 + juice: 9.1.0(encoding@0.1.13) + luxon: 3.7.2 + transitivePeerDependencies: + - encoding + + '@tryghost/kg-default-nodes@2.0.21(@noble/hashes@1.8.0)': + dependencies: + '@lexical/clipboard': 0.13.1(lexical@0.13.1) + '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) + '@lexical/selection': 0.13.1(lexical@0.13.1) + '@lexical/utils': 0.13.1(lexical@0.13.1) + '@tryghost/kg-clean-basic-html': 4.2.23 + '@tryghost/kg-markdown-html-renderer': 7.1.18 + clsx: 2.1.1 + html-minifier: 4.0.0 + jsdom: 29.0.1(@noble/hashes@1.8.0) + lexical: 0.13.1 + lodash: 4.18.1 + luxon: 3.7.2 + transitivePeerDependencies: + - '@noble/hashes' + - canvas + + '@tryghost/kg-default-transforms@1.2.44(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + dependencies: + '@lexical/list': 0.13.1(lexical@0.13.1) + '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) + '@lexical/utils': 0.13.1(lexical@0.13.1) + '@tryghost/kg-default-nodes': 2.0.21(@noble/hashes@1.8.0) + lexical: 0.13.1 + transitivePeerDependencies: + - '@lexical/clipboard' + - '@lexical/selection' + - '@noble/hashes' + - canvas + + '@tryghost/kg-html-to-lexical@1.2.45(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + dependencies: + '@lexical/clipboard': 0.13.1(lexical@0.13.1) + '@lexical/headless': 0.13.1(lexical@0.13.1) + '@lexical/html': 0.13.1(lexical@0.13.1) + '@lexical/link': 0.13.1(lexical@0.13.1) + '@lexical/list': 0.13.1(lexical@0.13.1) + '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) + '@tryghost/kg-default-nodes': 2.0.21(@noble/hashes@1.8.0) + '@tryghost/kg-default-transforms': 1.2.44(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + jsdom: 29.0.1(@noble/hashes@1.8.0) + lexical: 0.13.1 + transitivePeerDependencies: + - '@lexical/selection' + - '@lexical/utils' + - '@noble/hashes' + - canvas + + '@tryghost/kg-lexical-html-renderer@1.3.44(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0)': + dependencies: + '@lexical/clipboard': 0.13.1(lexical@0.13.1) + '@lexical/code': 0.13.1(lexical@0.13.1) + '@lexical/headless': 0.13.1(lexical@0.13.1) + '@lexical/link': 0.13.1(lexical@0.13.1) + '@lexical/list': 0.13.1(lexical@0.13.1) + '@lexical/rich-text': 0.13.1(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@lexical/utils@0.13.1(lexical@0.13.1))(lexical@0.13.1) + '@tryghost/kg-default-nodes': 2.0.21(@noble/hashes@1.8.0) + '@tryghost/kg-default-transforms': 1.2.44(@lexical/clipboard@0.13.1(lexical@0.13.1))(@lexical/selection@0.13.1(lexical@0.13.1))(@noble/hashes@1.8.0) + jsdom: 29.0.1(@noble/hashes@1.8.0) + lexical: 0.13.1 + transitivePeerDependencies: + - '@lexical/selection' + - '@lexical/utils' + - '@noble/hashes' + - canvas + + '@tryghost/kg-markdown-html-renderer@7.1.18': + dependencies: + '@tryghost/kg-utils': 1.0.43 + markdown-it: 14.1.1 + markdown-it-footnote: 4.0.0 + markdown-it-image-lazy-loading: 2.0.1 + markdown-it-lazy-headers: 0.1.3 + markdown-it-mark: 4.0.0 + markdown-it-sub: 2.0.0 + markdown-it-sup: 2.0.0 + semver: 7.7.4 + + '@tryghost/kg-mobiledoc-html-renderer@7.1.18': + dependencies: + '@tryghost/kg-utils': 1.0.43 + mobiledoc-dom-renderer: 0.7.2 + simple-dom: 1.4.0 + + '@tryghost/kg-parser-plugins@4.2.25': + dependencies: + '@tryghost/kg-clean-basic-html': 4.2.23 + optional: true + + '@tryghost/kg-unsplash-selector@0.3.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tryghost/kg-utils@1.0.43': + dependencies: + semver: 7.7.4 + + '@tryghost/koenig-lexical@1.7.30': {} + + '@tryghost/limit-service@1.5.2': + dependencies: + '@tryghost/errors': 1.3.13 + lodash: 4.17.23 + luxon: 3.7.2 + transitivePeerDependencies: + - supports-color + + '@tryghost/logging@2.5.5': + dependencies: + '@tryghost/bunyan-rotating-filestream': 0.0.7 + '@tryghost/elasticsearch': 3.0.29 + '@tryghost/http-stream': 0.1.42 + '@tryghost/pretty-stream': 0.2.5 + '@tryghost/root-utils': 0.3.38 + bunyan: 1.8.15 + bunyan-loggly: 2.0.1 + fs-extra: 11.3.0 + gelf-stream: 1.1.1 + json-stringify-safe: 5.0.1 + lodash: 4.17.21 + transitivePeerDependencies: + - supports-color + + '@tryghost/members-csv@2.0.5': + dependencies: + fs-extra: 11.3.0 + lodash: 4.17.21 + papaparse: 5.3.2 + pump: 3.0.2 + + '@tryghost/metrics@1.0.43': + dependencies: + '@tryghost/elasticsearch': 3.0.29 + '@tryghost/pretty-stream': 0.2.5 + '@tryghost/root-utils': 0.3.38 + json-stringify-safe: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@tryghost/mobiledoc-kit@0.12.4-ghost.1': + dependencies: + mobiledoc-dom-renderer: 0.7.0 + mobiledoc-text-renderer: 0.4.0 + optional: true + + '@tryghost/mongo-knex@0.9.4': + dependencies: + debug: 4.4.3(supports-color@5.5.0) + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/mongo-utils@0.6.3': + dependencies: + lodash: 4.17.23 + + '@tryghost/mw-error-handler@1.0.13': + dependencies: + '@tryghost/debug': 0.1.40 + '@tryghost/errors': 1.3.13 + '@tryghost/http-cache-utils': 0.1.25 + '@tryghost/tpl': 0.1.40 + lodash: 4.17.23 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + '@tryghost/mw-vhost@1.0.6': {} + + '@tryghost/nodemailer@0.3.48(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8)': + dependencies: + '@aws-sdk/client-ses': 3.1018.0 + '@tryghost/errors': 1.3.13 + nodemailer: 6.10.1 + nodemailer-direct-transport: 3.3.2 + nodemailer-mailgun-transport: 2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + nodemailer-stub-transport: 1.1.0 + transitivePeerDependencies: + - arc-templates + - atpl + - aws-crt + - babel-core + - bracket-template + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + + '@tryghost/nql-lang@0.6.4': + dependencies: + date-fns: 2.30.0 + + '@tryghost/nql@0.12.10': + dependencies: + '@tryghost/mongo-knex': 0.9.4 + '@tryghost/mongo-utils': 0.6.3 + '@tryghost/nql-lang': 0.6.4 + mingo: 2.5.3 + transitivePeerDependencies: + - supports-color + + '@tryghost/pretty-cli@1.2.52': + dependencies: + chalk: 5.6.2 + sywac: 1.3.0 + + '@tryghost/pretty-cli@3.0.3': + dependencies: + chalk: 5.6.2 + sywac: 1.3.0 + + '@tryghost/pretty-stream@0.2.5': + dependencies: + date-format: 4.0.14 + lodash: 4.17.23 + prettyjson: 1.2.5 + + '@tryghost/prometheus-metrics@1.0.8': + dependencies: + '@tryghost/logging': 2.5.5 + express: 4.22.1 + prom-client: 15.1.3 + stoppable: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@tryghost/promise@0.3.20': {} + + '@tryghost/promise@0.3.8': {} + + '@tryghost/referrer-parser@0.1.15': {} + + '@tryghost/request@1.0.12': + dependencies: + '@tryghost/errors': 1.3.13 + '@tryghost/validator': 0.2.22 + '@tryghost/version': 0.1.38 + cacheable-lookup: 7.0.0 + got: 13.0.0 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/request@1.0.17': + dependencies: + '@tryghost/errors': 1.3.13 + '@tryghost/validator': 0.2.22 + '@tryghost/version': 0.1.38 + cacheable-lookup: 7.0.0 + got: 14.6.6 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@tryghost/root-utils@0.3.38': + dependencies: + caller: 1.1.0 + find-root: 1.1.0 + + '@tryghost/root-utils@2.0.3': + dependencies: + caller: 1.1.0 + find-root: 1.1.0 + + '@tryghost/security@1.0.6': + dependencies: + '@tryghost/string': 0.2.21 + bcryptjs: 3.0.3 + + '@tryghost/server@2.0.3': + dependencies: + '@tryghost/debug': 2.0.3 + '@tryghost/logging': 2.5.5 + transitivePeerDependencies: + - supports-color + + '@tryghost/social-urls@0.1.60': {} + + '@tryghost/string@0.2.21': + dependencies: + unidecode: 0.1.8 + + '@tryghost/string@0.3.2': + dependencies: + unidecode: 1.1.0 + + '@tryghost/timezone-data@0.4.18': {} + + '@tryghost/tpl@0.1.40': + dependencies: + lodash.template: 4.5.0 + + '@tryghost/tpl@2.0.3': {} + + '@tryghost/url-utils@5.1.2': + dependencies: + cheerio: 0.22.0 + lodash: 4.17.23 + moment: 2.24.0 + moment-timezone: 0.5.45 + remark: 11.0.2 + remark-footnotes: 1.0.0 + unist-util-visit: 2.0.3 + + '@tryghost/url-utils@5.2.2': + dependencies: + cheerio: 1.2.0 + lodash: 4.17.23 + moment: 2.24.0 + moment-timezone: 0.5.45 + remark: 11.0.2 + remark-footnotes: 1.0.0 + unist-util-visit: 2.0.3 + + '@tryghost/validator@0.2.22': + dependencies: + '@tryghost/errors': 1.3.13 + '@tryghost/tpl': 0.1.40 + lodash: 4.17.23 + moment-timezone: 0.5.45 + validator: 7.2.0 + transitivePeerDependencies: + - supports-color + + '@tryghost/version@0.1.38': + dependencies: + '@tryghost/root-utils': 0.3.38 + semver: 7.7.4 + + '@tryghost/webhook-mock-receiver@0.2.22': + dependencies: + p-wait-for: 3.2.0 + + '@tryghost/zip@1.1.54': + dependencies: + '@tryghost/errors': 1.3.13 + archiver: 5.3.2 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@tryghost/zip@3.0.3': + dependencies: + '@tryghost/errors': 1.3.13 + archiver: 7.0.1 + extract-zip: 2.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.8 + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/bluebird@3.5.42': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.6.0 + + '@types/bookshelf@1.2.9(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)': + dependencies: + '@types/bluebird': 3.5.42 + '@types/create-error': 0.3.33 + '@types/lodash': 4.17.24 + knex: 0.21.21(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + transitivePeerDependencies: + - mssql + - mysql + - mysql2 + - pg + - sqlite3 + - supports-color + + '@types/broccoli-plugin@1.3.0': {} + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 25.6.0 + '@types/responselike': 1.0.3 + + '@types/chai-as-promised@7.1.8': + dependencies: + '@types/chai': 5.2.3 + + '@types/chai@4.3.20': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/color-convert@2.0.4': + dependencies: + '@types/color-name': 1.1.5 + + '@types/color-convert@3.0.1': + dependencies: + color-convert: 3.1.3 + + '@types/color-name@1.1.5': {} + + '@types/color@4.2.0': + dependencies: + '@types/color-convert': 3.0.1 + + '@types/color@4.2.1': + dependencies: + '@types/color-convert': 2.0.4 + + '@types/common-tags@1.8.4': {} + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.6.0 + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.19': + dependencies: + '@types/node': 25.6.0 + + '@types/create-error@0.3.33': {} + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/deep-eql@4.0.2': {} + + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 25.6.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@3.3.47': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 25.6.0 + '@types/ssh2': 1.15.5 + + '@types/doctrine@0.0.9': {} + + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@8.56.12': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 25.6.0 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.15.0 + '@types/serve-static': 1.15.10 + + '@types/find-cache-dir@3.2.1': {} + + '@types/fs-extra@5.1.0': + dependencies: + '@types/node': 25.6.0 + + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 25.6.0 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 6.0.0 + '@types/node': 25.6.0 + + '@types/glob@9.0.0': + dependencies: + glob: 10.5.0 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 25.6.0 + + '@types/http-cache-semantics@4.2.0': {} + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/jsdom@28.0.1': + dependencies: + '@types/node': 25.6.0 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + undici-types: 7.24.7 + + '@types/json-schema@7.0.15': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.6.0 + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 25.6.0 + + '@types/linkify-it@5.0.0': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/mdx@2.0.13': {} + + '@types/methods@1.1.4': {} + + '@types/mime-types@3.0.1': {} + + '@types/mime@1.3.5': {} + + '@types/minimatch@3.0.5': {} + + '@types/minimatch@6.0.0': + dependencies: + minimatch: 10.2.4 + + '@types/minimist@1.2.5': {} + + '@types/mocha@10.0.10': {} + + '@types/ms@2.1.0': {} + + '@types/mysql@2.15.27': + dependencies: + '@types/node': 25.6.0 + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.6.0 + form-data: 4.0.5 + + '@types/node-jose@1.1.13': + dependencies: + '@types/node': 25.6.0 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/node@9.6.61': {} + + '@types/nodemailer@6.4.23': + dependencies: + '@types/node': 25.6.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/on-headers@1.0.4': + dependencies: + '@types/node': 25.6.0 + + '@types/parse-json@4.0.2': {} + + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.15.6 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 25.6.0 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + + '@types/prettier@2.7.3': {} + + '@types/pretty-hrtime@1.0.3': {} + + '@types/prop-types@15.7.15': {} + + '@types/q@1.5.8': {} + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@17.0.26(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react-svg-map@2.1.4': + dependencies: + '@types/react': 18.3.28 + + '@types/react-transition-group@4.4.12(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react-world-flags@1.6.0': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/resolve@1.20.6': {} + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 25.6.0 + + '@types/rimraf@2.0.5': + dependencies: + '@types/glob': 9.0.0 + '@types/node': 25.6.0 + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 25.6.0 + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.6.0 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.6.0 + '@types/send': 0.17.6 + + '@types/sinon@17.0.4': + dependencies: + '@types/sinonjs__fake-timers': 15.0.1 + + '@types/sinonjs__fake-timers@15.0.1': {} + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + + '@types/stack-utils@2.0.3': {} + + '@types/statuses@2.0.6': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 25.6.0 + form-data: 4.0.5 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + + '@types/symlink-or-copy@1.2.2': {} + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.6.0 + + '@types/testing-library__jest-dom@5.14.9': + dependencies: + '@types/jest': 29.5.14 + + '@types/tough-cookie@4.0.5': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/uuid@9.0.8': {} + + '@types/validator@13.15.10': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.6.0 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.6.0 + optional: true + + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/type-utils': 8.49.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 9.37.0(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3(supports-color@5.5.0) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.49.0': + dependencies: + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/scope-manager@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.49.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 8.57.1 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.58.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 8.57.1 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.37.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.49.0': {} + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/types@8.58.0': {} + + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 9.0.9 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.49.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.37.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.49.0': + dependencies: + '@typescript-eslint/types': 8.49.0 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + + '@uiw/codemirror-extensions-basic-setup@4.25.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0)': + dependencies: + '@codemirror/autocomplete': 6.20.1 + '@codemirror/commands': 6.10.3 + '@codemirror/language': 6.12.3 + '@codemirror/lint': 6.9.5 + '@codemirror/search': 6.6.0 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.40.0 + + '@uiw/react-codemirror@4.25.2(@babel/runtime@7.29.2)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@5.48.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@codemirror/commands': 6.10.3 + '@codemirror/state': 6.6.0 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.40.0 + '@uiw/codemirror-extensions-basic-setup': 4.25.2(@codemirror/autocomplete@6.20.1)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0) + codemirror: 5.48.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react-swc@4.1.0(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.35 + '@swc/core': 1.15.21 + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@swc/helpers' + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@0.34.6(vitest@1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + picocolors: 1.1.1 + std-env: 3.10.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.3.0 + vitest: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@3.2.4(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.14(@types/node@25.6.0)(typescript@5.9.3) + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@4.1.2': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.21 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/spy@4.1.2': {} + + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/ast@1.9.0': + dependencies: + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/floating-point-hex-parser@1.9.0': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.9.0': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-buffer@1.9.0': {} + + '@webassemblyjs/helper-code-frame@1.9.0': + dependencies: + '@webassemblyjs/wast-printer': 1.9.0 + + '@webassemblyjs/helper-fsm@1.9.0': {} + + '@webassemblyjs/helper-module-context@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-bytecode@1.9.0': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/helper-wasm-section@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/ieee754@1.9.0': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/leb128@1.9.0': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/utf8@1.9.0': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-edit@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/helper-wasm-section': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-opt': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + '@webassemblyjs/wast-printer': 1.9.0 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-gen@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-opt@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-parser@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + + '@webassemblyjs/wast-parser@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/floating-point-hex-parser': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-code-frame': 1.9.0 + '@webassemblyjs/helper-fsm': 1.9.0 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/wast-printer@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + '@xtuc/long': 4.2.2 + + '@xmldom/xmldom@0.8.11': {} + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@yarnpkg/lockfile@1.1.0': {} + + '@yarnpkg/parsers@3.0.2': + dependencies: + js-yaml: 3.14.2 + tslib: 2.8.1 + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + abab@2.0.6: {} + + abbrev@1.1.1: {} + + abbrev@2.0.0: + optional: true + + abbrev@4.0.0: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abortcontroller-polyfill@1.7.8: {} + + abstract-leveldown@0.12.4: + dependencies: + xtend: 3.0.0 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-dynamic-import@3.0.0: + dependencies: + acorn: 5.7.4 + + acorn-globals@6.0.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@7.2.0: {} + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@5.7.4: {} + + acorn@6.4.2: {} + + acorn@7.4.1: {} + + acorn@8.16.0: {} + + adjust-sourcemap-loader@4.0.0: + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.3.1 + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ajv-errors@1.0.1(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv-keywords@5.1.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + fast-deep-equal: 3.1.3 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alphanum-sort@1.0.2: {} + + amd-name-resolver@1.2.0: + dependencies: + ensure-posix-path: 1.1.1 + + amd-name-resolver@1.3.1: + dependencies: + ensure-posix-path: 1.1.1 + object-hash: 1.3.1 + + amdefine@1.0.1: {} + + animejs@3.2.2: {} + + ansi-colors@4.1.3: {} + + ansi-escapes@3.2.0: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-html@0.0.7: {} + + ansi-regex@2.1.1: {} + + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@2.2.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansi-to-html@0.6.15: + dependencies: + entities: 2.2.0 + + ansicolors@0.2.1: {} + + any-promise@1.3.0: {} + + anymatch@2.0.0: + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + app-root-dir@1.0.2: {} + + app-root-path@2.2.1: {} + + append-field@1.0.0: {} + + applause@2.0.4: + dependencies: + lodash: 4.17.23 + optional-require: 1.1.10 + optionalDependencies: + cson-parser: 4.0.9 + js-yaml: 4.1.1 + + aproba@1.2.0: {} + + aproba@2.1.0: + optional: true + + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.23 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.8 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + are-we-there-yet@1.1.7: + dependencies: + delegates: 1.0.0 + readable-stream: 2.3.8 + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@4.1.3: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-each@1.0.1: {} + + array-equal@1.0.2: {} + + array-flatten@1.1.1: {} + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-slice@1.1.0: {} + + array-to-error@1.1.1: + dependencies: + array-to-sentence: 1.1.0 + + array-to-sentence@1.1.0: {} + + array-union@2.1.0: {} + + array-unique@0.3.2: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.reduce@1.0.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-array-method-boxes-properly: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + is-string: 1.1.1 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + arrify@1.0.1: {} + + asap@2.0.6: {} + + asn1.js@4.10.1: + dependencies: + bn.js: 4.12.3 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.3 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-never@1.4.0: {} + + assert-plus@1.0.0: {} + + assert@1.5.1: + dependencies: + object.assign: 4.1.7 + util: 0.10.4 + + assertion-error@1.1.0: {} + + assertion-error@2.0.1: {} + + assign-symbols@1.0.0: {} + + ast-types@0.13.3: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + astral-regex@2.0.0: {} + + async-disk-cache@1.3.5: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + istextorbinary: 2.1.0 + mkdirp: 0.5.6 + rimraf: 2.7.1 + rsvp: 3.6.2 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color + + async-each@1.0.6: + optional: true + + async-function@1.0.0: {} + + async-promise-queue@1.0.5: + dependencies: + async: 2.6.4 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + async@0.2.10: {} + + async@2.6.4: + dependencies: + lodash: 4.17.23 + + async@3.2.3: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + atob@2.1.2: {} + + attr-accept@2.2.5: {} + + audio-extensions@0.0.0: {} + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001781 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + autoprefixer@9.8.6: + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001781 + colorette: 1.4.0 + normalize-range: 0.1.2 + num2fraction: 1.2.2 + postcss: 7.0.39 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-sign2@0.7.0: {} + + aws-ssl-profiles@1.1.2: {} + + aws4@1.13.2: {} + + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + b4a@1.8.0: {} + + babel-code-frame@6.26.0: + dependencies: + chalk: 1.1.3 + esutils: 2.0.3 + js-tokens: 3.0.2 + + babel-core@6.26.3: + dependencies: + babel-code-frame: 6.26.0 + babel-generator: 6.26.1 + babel-helpers: 6.24.1 + babel-messages: 6.23.0 + babel-register: 6.26.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + convert-source-map: 1.9.0 + debug: 2.6.9 + json5: 0.5.1 + lodash: 4.17.23 + minimatch: 3.1.5 + path-is-absolute: 1.0.1 + private: 0.1.8 + slash: 1.0.0 + source-map: 0.5.7 + transitivePeerDependencies: + - supports-color + + babel-generator@6.26.1: + dependencies: + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + detect-indent: 4.0.0 + jsesc: 1.3.0 + lodash: 4.17.23 + source-map: 0.5.7 + trim-right: 1.0.1 + + babel-helper-builder-binary-assignment-operator-visitor@6.24.1: + dependencies: + babel-helper-explode-assignable-expression: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-builder-react-jsx@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + esutils: 2.0.3 + + babel-helper-call-delegate@6.24.1: + dependencies: + babel-helper-hoist-variables: 6.24.1 + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-define-map@6.26.0: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + babel-helper-explode-assignable-expression@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-function-name@6.24.1: + dependencies: + babel-helper-get-function-arity: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-get-function-arity@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-hoist-variables@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-optimise-call-expression@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-regex@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + lodash: 4.17.23 + + babel-helper-remap-async-to-generator@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-replace-supers@6.24.1: + dependencies: + babel-helper-optimise-call-expression: 6.24.1 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helpers@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-import-util@0.2.0: {} + + babel-import-util@1.4.1: {} + + babel-import-util@2.1.1: {} + + babel-import-util@3.0.1: {} + + babel-jest@29.7.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-loader@8.4.1(@babel/core@7.29.0)(webpack@4.47.0): + dependencies: + '@babel/core': 7.29.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 4.47.0 + + babel-loader@8.4.1(@babel/core@7.29.0)(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + '@babel/core': 7.29.0 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.105.4(@swc/core@1.15.21) + + babel-messages@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-check-es2015-constants@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-debug-macros@0.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + semver: 5.7.2 + + babel-plugin-debug-macros@0.3.4(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + semver: 5.7.2 + + babel-plugin-ember-data-packages-polyfill@0.1.2: + dependencies: + '@ember-data/rfc395-data': 0.0.4 + + babel-plugin-ember-modules-api-polyfill@2.13.4: + dependencies: + ember-rfc176-data: 0.3.18 + + babel-plugin-ember-modules-api-polyfill@3.5.0: + dependencies: + ember-rfc176-data: 0.3.18 + + babel-plugin-ember-template-compilation@2.4.1: + dependencies: + '@glimmer/syntax': 0.95.0 + babel-import-util: 3.0.1 + + babel-plugin-filter-imports@4.0.0: + dependencies: + '@babel/types': 7.29.0 + lodash: 4.17.23 + + babel-plugin-htmlbars-inline-precompile@1.0.0: {} + + babel-plugin-htmlbars-inline-precompile@3.2.0: {} + + babel-plugin-htmlbars-inline-precompile@5.3.1: + dependencies: + babel-plugin-ember-modules-api-polyfill: 3.5.0 + line-column: 1.0.2 + magic-string: 0.25.9 + parse-static-imports: 1.1.0 + string.prototype.matchall: 4.0.12 + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.2 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + babel-plugin-module-resolver@3.2.0: + dependencies: + find-babel-config: 1.2.2 + glob: 7.2.3 + pkg-up: 2.0.0 + reselect: 3.0.1 + resolve: 1.22.11 + + babel-plugin-module-resolver@4.1.0: + dependencies: + find-babel-config: 1.2.2 + glob: 7.2.3 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.11 + + babel-plugin-module-resolver@5.0.3: + dependencies: + find-babel-config: 2.1.2 + glob: 9.3.5 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.11 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-syntax-async-functions@6.13.0: {} + + babel-plugin-syntax-class-properties@6.13.0: {} + + babel-plugin-syntax-dynamic-import@6.18.0: {} + + babel-plugin-syntax-exponentiation-operator@6.13.0: {} + + babel-plugin-syntax-jsx@6.18.0: {} + + babel-plugin-syntax-trailing-function-commas@6.22.0: {} + + babel-plugin-transform-async-to-generator@6.24.1: + dependencies: + babel-helper-remap-async-to-generator: 6.24.1 + babel-plugin-syntax-async-functions: 6.13.0 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-class-properties@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-plugin-syntax-class-properties: 6.13.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-arrow-functions@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-block-scoped-functions@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-block-scoping@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-classes@6.24.1: + dependencies: + babel-helper-define-map: 6.26.0 + babel-helper-function-name: 6.24.1 + babel-helper-optimise-call-expression: 6.24.1 + babel-helper-replace-supers: 6.24.1 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-computed-properties@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-destructuring@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-duplicate-keys@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-for-of@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-function-name@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-literals@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-modules-amd@6.24.1: + dependencies: + babel-plugin-transform-es2015-modules-commonjs: 6.26.2 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-commonjs@6.26.2: + dependencies: + babel-plugin-transform-strict-mode: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-systemjs@6.24.1: + dependencies: + babel-helper-hoist-variables: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-umd@6.24.1: + dependencies: + babel-plugin-transform-es2015-modules-amd: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-object-super@6.24.1: + dependencies: + babel-helper-replace-supers: 6.24.1 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-parameters@6.24.1: + dependencies: + babel-helper-call-delegate: 6.24.1 + babel-helper-get-function-arity: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-shorthand-properties@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-spread@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-sticky-regex@6.24.1: + dependencies: + babel-helper-regex: 6.26.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-template-literals@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-typeof-symbol@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-unicode-regex@6.24.1: + dependencies: + babel-helper-regex: 6.26.0 + babel-runtime: 6.26.0 + regexpu-core: 2.0.0 + + babel-plugin-transform-exponentiation-operator@6.24.1: + dependencies: + babel-helper-builder-binary-assignment-operator-visitor: 6.24.1 + babel-plugin-syntax-exponentiation-operator: 6.13.0 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-react-jsx@6.24.1: + dependencies: + babel-helper-builder-react-jsx: 6.26.0 + babel-plugin-syntax-jsx: 6.18.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-regenerator@6.26.0: + dependencies: + regenerator-transform: 0.10.1 + + babel-plugin-transform-strict-mode@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-polyfill@6.26.0: + dependencies: + babel-runtime: 6.26.0 + core-js: 2.6.12 + regenerator-runtime: 0.10.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-env@1.7.0: + dependencies: + babel-plugin-check-es2015-constants: 6.22.0 + babel-plugin-syntax-trailing-function-commas: 6.22.0 + babel-plugin-transform-async-to-generator: 6.24.1 + babel-plugin-transform-es2015-arrow-functions: 6.22.0 + babel-plugin-transform-es2015-block-scoped-functions: 6.22.0 + babel-plugin-transform-es2015-block-scoping: 6.26.0 + babel-plugin-transform-es2015-classes: 6.24.1 + babel-plugin-transform-es2015-computed-properties: 6.24.1 + babel-plugin-transform-es2015-destructuring: 6.23.0 + babel-plugin-transform-es2015-duplicate-keys: 6.24.1 + babel-plugin-transform-es2015-for-of: 6.23.0 + babel-plugin-transform-es2015-function-name: 6.24.1 + babel-plugin-transform-es2015-literals: 6.22.0 + babel-plugin-transform-es2015-modules-amd: 6.24.1 + babel-plugin-transform-es2015-modules-commonjs: 6.26.2 + babel-plugin-transform-es2015-modules-systemjs: 6.24.1 + babel-plugin-transform-es2015-modules-umd: 6.24.1 + babel-plugin-transform-es2015-object-super: 6.24.1 + babel-plugin-transform-es2015-parameters: 6.24.1 + babel-plugin-transform-es2015-shorthand-properties: 6.24.1 + babel-plugin-transform-es2015-spread: 6.22.0 + babel-plugin-transform-es2015-sticky-regex: 6.24.1 + babel-plugin-transform-es2015-template-literals: 6.22.0 + babel-plugin-transform-es2015-typeof-symbol: 6.23.0 + babel-plugin-transform-es2015-unicode-regex: 6.24.1 + babel-plugin-transform-exponentiation-operator: 6.24.1 + babel-plugin-transform-regenerator: 6.26.0 + browserslist: 3.2.8 + invariant: 2.2.4 + semver: 5.7.2 + transitivePeerDependencies: + - supports-color + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + babel-register@6.26.0: + dependencies: + babel-core: 6.26.3 + babel-runtime: 6.26.0 + core-js: 2.6.12 + home-or-tmp: 2.0.0 + lodash: 4.17.23 + mkdirp: 0.5.6 + source-map-support: 0.4.18 + transitivePeerDependencies: + - supports-color + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + + babel-template@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + babel-traverse@6.26.0: + dependencies: + babel-code-frame: 6.26.0 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + debug: 2.6.9 + globals: 9.18.0 + invariant: 2.2.4 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + babel-types@6.26.0: + dependencies: + babel-runtime: 6.26.0 + esutils: 2.0.3 + lodash: 4.17.23 + to-fast-properties: 1.0.3 + + babel6-plugin-strip-class-callcheck@6.0.0: {} + + babylon@6.18.0: {} + + backbone@1.6.1: + dependencies: + underscore: 1.13.8 + + bail@1.0.5: {} + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + balanced-match@4.0.4: {} + + bare-events@2.8.2: {} + + bare-fs@4.5.6: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.11.0(bare-events@2.8.2) + bare-url: 2.4.0 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.8.0: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.8.0 + + bare-stream@2.11.0(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.0: + dependencies: + bare-path: 3.0.0 + + base-64@1.0.0: {} + + base64-arraybuffer@0.2.0: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + base64url@3.0.1: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + baseline-browser-mapping@2.10.11: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + batch-processor@1.0.0: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + bcryptjs@3.0.3: {} + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + big.js@5.2.2: {} + + binary-extensions@1.13.1: + optional: true + + binary-extensions@2.3.0: {} + + binaryextensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + bintrees@1.0.2: {} + + bl@0.8.2: + dependencies: + readable-stream: 1.0.34 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bl@5.1.0: + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blank-object@1.0.2: {} + + bluebird@3.7.2: {} + + bn.js@4.12.3: {} + + bn.js@5.2.3: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + body@5.1.0: + dependencies: + continuable-cache: 0.3.1 + error: 7.2.1 + raw-body: 1.1.7 + safe-json-parse: 1.0.1 + + bookshelf-relations@2.8.0(bookshelf@1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7))): + dependencies: + '@tryghost/debug': 0.1.40 + '@tryghost/errors': 1.3.13 + bluebird: 3.7.2 + bookshelf: 1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)) + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + bookshelf@1.2.0(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)): + dependencies: + bluebird: 3.7.2 + create-error: 0.3.1 + inflection: 1.13.4 + knex: 2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + lodash: 4.17.23 + + boolbase@1.0.0: {} + + boolean@3.2.0: {} + + bower-config@1.4.3: + dependencies: + graceful-fs: 4.2.11 + minimist: 0.2.4 + mout: 1.2.4 + osenv: 0.1.5 + untildify: 2.1.0 + wordwrap: 0.0.3 + + bower-endpoint-parser@0.2.2: {} + + bowser@2.14.1: {} + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.3: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bree@6.5.0: + dependencies: + '@babel/runtime': 7.29.2 + '@breejs/later': 4.2.0 + boolean: 3.2.0 + bthreads: 0.5.1 + combine-errors: 3.0.3 + cron-validate: 1.4.5 + debug: 4.4.3(supports-color@5.5.0) + human-interval: 2.0.1 + is-string-and-not-blank: 0.0.2 + is-valid-path: 0.1.1 + ms: 2.1.3 + p-wait-for: 3.1.0 + safe-timers: 1.1.0 + transitivePeerDependencies: + - supports-color + + broccoli-amd-funnel@2.0.1: + dependencies: + broccoli-plugin: 1.3.1 + symlink-or-copy: 1.3.1 + + broccoli-asset-rev@3.0.0: + dependencies: + broccoli-asset-rewrite: 2.0.0 + broccoli-filter: 1.3.0 + broccoli-persistent-filter: 2.3.1 + json-stable-stringify: 1.3.0 + minimatch: 3.1.5 + rsvp: 3.6.2 + transitivePeerDependencies: + - supports-color + + broccoli-asset-rewrite@2.0.0: + dependencies: + broccoli-filter: 1.3.0 + transitivePeerDependencies: + - supports-color + + broccoli-babel-transpiler@6.5.1: + dependencies: + babel-core: 6.26.3 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 2.0.1 + broccoli-persistent-filter: 2.3.1 + clone: 2.1.2 + hash-for-dep: 1.5.2 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + rsvp: 4.8.5 + workerpool: 2.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-babel-transpiler@7.8.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/polyfill': 7.12.1 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + broccoli-persistent-filter: 2.3.1 + clone: 2.1.2 + hash-for-dep: 1.5.2 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + rsvp: 4.8.5 + workerpool: 3.1.2 + transitivePeerDependencies: + - supports-color + + broccoli-babel-transpiler@8.0.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + broccoli-persistent-filter: 2.3.1 + clone: 2.1.2 + hash-for-dep: 1.5.2 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + rsvp: 4.8.5 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + + broccoli-builder@0.18.14: + dependencies: + broccoli-node-info: 1.1.0 + heimdalljs: 0.2.6 + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + rsvp: 3.6.2 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + broccoli-caching-writer@3.1.0: + dependencies: + broccoli-plugin: 1.3.1 + debug: 3.2.7 + rimraf: 2.7.1 + rsvp: 3.6.2 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-clean-css@1.1.0: + dependencies: + broccoli-persistent-filter: 2.3.1 + clean-css-promise: 0.1.1 + inline-source-map-comment: 1.0.5 + json-stable-stringify: 1.3.0 + transitivePeerDependencies: + - supports-color + + broccoli-concat@4.2.7: + dependencies: + broccoli-debug: 0.6.5 + broccoli-plugin: 4.0.7 + ensure-posix-path: 1.1.1 + fast-sourcemap-concat: 2.1.1 + find-index: 1.1.1 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 + lodash: 4.17.23 + transitivePeerDependencies: + - supports-color + + broccoli-config-loader@1.0.1: + dependencies: + broccoli-caching-writer: 3.1.0 + transitivePeerDependencies: + - supports-color + + broccoli-config-replace@1.1.3: + dependencies: + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fs-extra: 0.24.0 + transitivePeerDependencies: + - supports-color + + broccoli-debug@0.6.5: + dependencies: + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 + tree-sync: 1.4.0 + transitivePeerDependencies: + - supports-color + + broccoli-file-creator@1.2.0: + dependencies: + broccoli-plugin: 1.3.1 + mkdirp: 0.5.6 + + broccoli-file-creator@2.1.1: + dependencies: + broccoli-plugin: 1.3.1 + mkdirp: 0.5.6 + + broccoli-filter@1.3.0: + dependencies: + broccoli-kitchen-sink-helpers: 0.3.1 + broccoli-plugin: 1.3.1 + copy-dereference: 1.0.0 + debug: 2.6.9 + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rsvp: 3.6.2 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-funnel-reducer@1.0.0: {} + + broccoli-funnel@1.2.0: + dependencies: + array-equal: 1.0.2 + blank-object: 1.0.2 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + exists-sync: 0.0.4 + fast-ordered-set: 1.0.3 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + minimatch: 3.1.5 + mkdirp: 0.5.6 + path-posix: 1.0.0 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-funnel@2.0.1: + dependencies: + array-equal: 1.0.2 + blank-object: 1.0.2 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fast-ordered-set: 1.0.3 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + minimatch: 3.1.5 + mkdirp: 0.5.6 + path-posix: 1.0.0 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-funnel@2.0.2: + dependencies: + array-equal: 1.0.2 + blank-object: 1.0.2 + broccoli-plugin: 1.3.1 + debug: 2.6.9 + fast-ordered-set: 1.0.3 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + minimatch: 3.1.5 + mkdirp: 0.5.6 + path-posix: 1.0.0 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-funnel@3.0.8: + dependencies: + array-equal: 1.0.2 + broccoli-plugin: 4.0.7 + debug: 4.4.3(supports-color@5.5.0) + fs-tree-diff: 2.0.1 + heimdalljs: 0.2.6 + minimatch: 3.1.5 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + broccoli-kitchen-sink-helpers@0.3.1: + dependencies: + glob: 5.0.15 + mkdirp: 0.5.6 + + broccoli-merge-trees@1.2.4: + dependencies: + broccoli-plugin: 1.3.1 + can-symlink: 1.0.0 + fast-ordered-set: 1.0.3 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-merge-trees@2.0.1: + dependencies: + broccoli-plugin: 1.3.1 + merge-trees: 1.0.1 + transitivePeerDependencies: + - supports-color + + broccoli-merge-trees@3.0.2: + dependencies: + broccoli-plugin: 1.3.1 + merge-trees: 2.0.0 + transitivePeerDependencies: + - supports-color + + broccoli-merge-trees@4.2.0: + dependencies: + broccoli-plugin: 4.0.7 + merge-trees: 2.0.0 + transitivePeerDependencies: + - supports-color + + broccoli-middleware@2.1.1: + dependencies: + ansi-html: 0.0.7 + handlebars: 4.7.9 + has-ansi: 3.0.0 + mime-types: 2.1.35 + + broccoli-node-api@1.7.0: {} + + broccoli-node-info@1.1.0: {} + + broccoli-node-info@2.2.0: {} + + broccoli-output-wrapper@2.0.0: + dependencies: + heimdalljs-logger: 0.1.10 + transitivePeerDependencies: + - supports-color + + broccoli-output-wrapper@3.2.5: + dependencies: + fs-extra: 8.1.0 + heimdalljs-logger: 0.1.10 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-persistent-filter@2.3.1: + dependencies: + async-disk-cache: 1.3.5 + async-promise-queue: 1.0.5 + broccoli-plugin: 1.3.1 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.2 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + mkdirp: 0.5.6 + promise-map-series: 0.2.3 + rimraf: 2.7.1 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + sync-disk-cache: 1.3.4 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color + + broccoli-plugin@1.3.1: + dependencies: + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + + broccoli-plugin@2.1.0: + dependencies: + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + + broccoli-plugin@3.1.0: + dependencies: + broccoli-node-api: 1.7.0 + broccoli-output-wrapper: 2.0.0 + fs-merger: 3.2.1 + promise-map-series: 0.2.3 + quick-temp: 0.1.9 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-plugin@4.0.7: + dependencies: + broccoli-node-api: 1.7.0 + broccoli-output-wrapper: 3.2.5 + fs-merger: 3.2.1 + promise-map-series: 0.3.0 + quick-temp: 0.1.9 + rimraf: 3.0.2 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + broccoli-postcss-single@4.0.1: + dependencies: + broccoli-caching-writer: 3.1.0 + include-path-searcher: 0.1.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + object-assign: 4.1.1 + postcss: 7.0.39 + transitivePeerDependencies: + - supports-color + + broccoli-postcss@5.1.0: + dependencies: + broccoli-funnel: 3.0.8 + broccoli-persistent-filter: 2.3.1 + minimist: 1.2.8 + object-assign: 4.1.1 + postcss: 7.0.39 + transitivePeerDependencies: + - supports-color + + broccoli-postcss@6.1.0: + dependencies: + broccoli-funnel: 3.0.8 + broccoli-persistent-filter: 2.3.1 + minimist: 1.2.8 + object-assign: 4.1.1 + postcss: 8.5.6 + transitivePeerDependencies: + - supports-color + + broccoli-replace@2.0.2: + dependencies: + applause: 2.0.4 + broccoli-filter: 1.3.0 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + broccoli-rollup@2.1.1: + dependencies: + '@types/node': 9.6.61 + amd-name-resolver: 1.3.1 + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + magic-string: 0.24.1 + node-modules-path: 1.0.2 + rollup: 0.57.1 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-rollup@4.1.1: + dependencies: + '@types/broccoli-plugin': 1.3.0 + broccoli-plugin: 2.1.0 + fs-tree-diff: 2.0.1 + heimdalljs: 0.2.6 + node-modules-path: 1.0.2 + rollup: 1.32.1 + rollup-pluginutils: 2.8.2 + symlink-or-copy: 1.3.1 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color + + broccoli-slow-trees@3.1.0: + dependencies: + heimdalljs: 0.2.6 + + broccoli-source@1.1.0: {} + + broccoli-source@2.1.2: {} + + broccoli-source@3.0.1: + dependencies: + broccoli-node-api: 1.7.0 + + broccoli-stew@1.6.0: + dependencies: + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 2.0.1 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 1.3.1 + chalk: 2.4.2 + debug: 3.2.7 + ensure-posix-path: 1.1.1 + fs-extra: 5.0.0 + minimatch: 3.1.5 + resolve: 1.22.11 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-stew@3.0.0: + dependencies: + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 2.1.0 + chalk: 2.4.2 + debug: 4.4.3(supports-color@5.5.0) + ensure-posix-path: 1.1.1 + fs-extra: 8.1.0 + minimatch: 3.1.5 + resolve: 1.22.11 + rsvp: 4.8.5 + symlink-or-copy: 1.3.1 + walk-sync: 1.1.4 + transitivePeerDependencies: + - supports-color + + broccoli-string-replace@0.1.2: + dependencies: + broccoli-persistent-filter: 2.3.1 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + broccoli-svg-optimizer@2.1.0: + dependencies: + broccoli-persistent-filter: 2.3.1 + safe-stable-stringify: 2.5.0 + svgo: 1.3.0 + transitivePeerDependencies: + - supports-color + + broccoli-templater@2.0.2: + dependencies: + broccoli-plugin: 1.3.1 + fs-tree-diff: 0.5.9 + lodash.template: 4.5.0 + rimraf: 2.7.1 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + broccoli-terser-sourcemap@4.1.1: + dependencies: + async-promise-queue: 1.0.5 + broccoli-plugin: 4.0.7 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + lodash.defaultsdeep: 4.6.1 + matcher-collection: 2.0.1 + symlink-or-copy: 1.3.1 + terser: 5.44.0 + walk-sync: 2.2.0 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + + broccoli@3.5.2: + dependencies: + '@types/chai': 4.3.20 + '@types/chai-as-promised': 7.1.8 + '@types/express': 4.17.25 + ansi-html: 0.0.7 + broccoli-node-info: 2.2.0 + broccoli-slow-trees: 3.1.0 + broccoli-source: 3.0.1 + commander: 4.1.1 + connect: 3.7.0 + console-ui: 3.1.2 + esm: 3.2.25 + findup-sync: 4.0.0 + handlebars: 4.7.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + https: 1.0.0 + mime-types: 2.1.35 + resolve-path: 1.4.0 + rimraf: 3.0.2 + sane: 4.1.0 + tmp: 0.0.33 + tree-sync: 2.1.0 + underscore.string: 3.3.6 + watch-detector: 1.0.2 + transitivePeerDependencies: + - supports-color + + brorand@1.1.0: {} + + browser-assert@1.2.1: {} + + browser-process-hrtime@1.0.0: {} + + browser-stdout@1.3.1: {} + + browserify-aes@1.2.0: + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.7 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-cipher@1.0.1: + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + + browserify-des@1.0.2: + dependencies: + cipher-base: 1.0.7 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-fs@1.0.0: + dependencies: + level-filesystem: 1.2.0 + level-js: 2.2.4 + levelup: 0.18.6 + + browserify-rsa@4.1.1: + dependencies: + bn.js: 5.2.3 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + browserify-sign@4.2.5: + dependencies: + bn.js: 5.2.3 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.6.1 + inherits: 2.0.4 + parse-asn1: 5.1.9 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + + browserslist@3.2.8: + dependencies: + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.328 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.11 + caniuse-lite: 1.0.30001781 + electron-to-chromium: 1.5.328 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + brute-knex@4.0.1(express@4.21.2)(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7): + dependencies: + express-brute: 1.0.1(express@4.21.2) + knex: 0.20.15(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + transitivePeerDependencies: + - express + - mssql + - mysql + - mysql2 + - pg + - sqlite3 + - supports-color + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + bson-objectid@2.0.4: {} + + bthreads@0.5.1: + dependencies: + bufio: 1.0.7 + + buffer-crc32@0.2.13: {} + + buffer-crc32@1.0.0: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-es6@4.9.3: {} + + buffer-from@1.1.2: {} + + buffer-xor@1.0.3: {} + + buffer@4.9.2: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufio@1.0.7: {} + + buildcheck@0.0.7: + optional: true + + builtin-modules@3.3.0: {} + + builtin-status-codes@3.0.0: {} + + builtins@1.0.3: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bunyan-loggly@2.0.1: + dependencies: + json-stringify-safe: 5.0.1 + node-loggly-bulk: 3.0.1 + + bunyan@1.8.15: + optionalDependencies: + dtrace-provider: 0.8.8 + moment: 2.24.0 + mv: 2.1.1 + safe-json-stringify: 1.2.0 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + byte-counter@0.1.0: {} + + bytes@1.0.0: {} + + bytes@3.1.2: {} + + c8@10.1.3: + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 3.3.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + test-exclude: 7.0.2 + v8-to-istanbul: 9.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + + cac@6.7.14: {} + + cacache@12.0.4: + dependencies: + bluebird: 3.7.2 + chownr: 1.1.4 + figgy-pudding: 3.5.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + infer-owner: 1.0.4 + lru-cache: 5.1.1 + mississippi: 3.0.0 + mkdirp: 0.5.6 + move-concurrently: 1.0.1 + promise-inflight: 1.0.1(bluebird@3.7.2) + rimraf: 2.7.1 + ssri: 6.0.2 + unique-filename: 1.1.1 + y18n: 4.0.3 + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1(bluebird@3.7.2) + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + cacache@20.0.4: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.6 + lru-cache: 11.2.7 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + p-map: 7.0.4 + ssri: 13.0.1 + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + cache-manager-ioredis@2.1.0: + dependencies: + ioredis: 4.31.0 + transitivePeerDependencies: + - supports-color + + cache-manager@4.1.0: + dependencies: + async: 3.2.3 + lodash.clonedeep: 4.5.0 + lru-cache: 7.18.3 + + cacheable-lookup@5.0.4: {} + + cacheable-lookup@7.0.0: {} + + cacheable-request@10.2.14: + dependencies: + '@types/http-cache-semantics': 4.2.0 + get-stream: 6.0.1 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.1.1 + responselike: 3.0.0 + + cacheable-request@13.0.18: + dependencies: + '@types/http-cache-semantics': 4.2.0 + get-stream: 9.0.1 + http-cache-semantics: 4.2.0 + keyv: 5.6.0 + mimic-response: 4.0.0 + normalize-url: 8.1.1 + responselike: 4.0.2 + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + calculate-cache-key-for-tree@2.0.0: + dependencies: + json-stable-stringify: 1.3.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caller-callsite@2.0.0: + dependencies: + callsites: 2.0.0 + + caller-path@2.0.0: + dependencies: + caller-callsite: 2.0.0 + + caller@1.1.0: {} + + callsites@2.0.0: {} + + callsites@3.1.0: {} + + camel-case@3.0.0: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + camelcase-css@2.0.1: {} + + camelcase-keys@7.0.2: + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + can-symlink@1.0.0: + dependencies: + tmp: 0.0.28 + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.1 + caniuse-lite: 1.0.30001781 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001781: {} + + capture-exit@2.0.0: + dependencies: + rsvp: 4.8.5 + + cardinal@1.0.0: + dependencies: + ansicolors: 0.2.1 + redeyed: 1.0.1 + + caseless@0.12.0: {} + + ccount@1.1.0: {} + + chai-dom@1.12.1(chai@4.5.0): + dependencies: + chai: 4.5.0 + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chai@6.2.2: {} + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + char-regex@1.0.2: {} + + character-entities-html4@1.1.4: {} + + character-entities-legacy@1.1.4: {} + + character-entities@1.2.4: {} + + character-reference-invalid@1.1.4: {} + + chardet@0.7.0: {} + + chardet@2.1.1: {} + + charm@1.0.2: + dependencies: + inherits: 2.0.4 + + charset@1.0.1: {} + + chart.js@2.9.4: + dependencies: + chartjs-color: 2.4.1 + moment: 2.24.0 + + chartjs-color-string@0.6.0: + dependencies: + color-name: 1.1.4 + + chartjs-color@2.4.1: + dependencies: + chartjs-color-string: 0.6.0 + color-convert: 1.9.3 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + check-error@2.1.3: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@0.22.0: + dependencies: + css-select: 1.2.0 + dom-serializer: 0.1.1 + entities: 1.1.2 + htmlparser2: 3.10.1 + lodash.assignin: 4.2.0 + lodash.bind: 4.2.1 + lodash.defaults: 4.2.0 + lodash.filter: 4.6.0 + lodash.flatten: 4.4.0 + lodash.foreach: 4.5.0 + lodash.map: 4.6.0 + lodash.merge: 4.6.2 + lodash.pick: 4.4.0 + lodash.reduce: 4.6.0 + lodash.reject: 4.6.0 + lodash.some: 4.6.0 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + htmlparser2: 8.0.2 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.24.6 + whatwg-mimetype: 4.0.0 + + chokidar@2.1.8: + dependencies: + anymatch: 2.0.0 + async-each: 1.0.6 + braces: 2.3.2 + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + optional: true + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + chownr@2.0.0: + optional: true + + chownr@3.0.0: {} + + chrome-trace-event@1.0.4: {} + + chrono-node@2.9.0: {} + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + cjs-module-lexer@1.4.3: {} + + cjs-module-lexer@2.2.0: {} + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clean-base-url@1.0.0: {} + + clean-css-promise@0.1.1: + dependencies: + array-to-error: 1.1.1 + clean-css: 3.4.28 + pinkie-promise: 2.0.1 + + clean-css@3.4.28: + dependencies: + commander: 2.8.1 + source-map: 0.4.4 + + clean-css@4.2.4: + dependencies: + source-map: 0.6.1 + + clean-regexp@1.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + clean-stack@2.2.0: {} + + clean-stack@3.0.1: + dependencies: + escape-string-regexp: 4.0.0 + + clean-up-path@1.0.0: {} + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + + cli-spinners@2.6.1: {} + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-table@0.3.11: + dependencies: + colors: 1.0.3 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + + cli-width@2.2.1: {} + + cli-width@3.0.0: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@0.1.19: {} + + clone@1.0.4: {} + + clone@2.1.2: {} + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + co@4.6.0: {} + + coa@2.0.2: + dependencies: + '@types/q': 1.5.8 + chalk: 2.4.2 + q: 1.5.1 + + code-point-at@1.1.0: {} + + codemirror@5.48.2: {} + + coffeescript@1.12.7: + optional: true + + collapse-white-space@1.0.6: {} + + collect-v8-coverage@1.0.3: {} + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color-support@1.1.3: + optional: true + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + colord@2.9.3: {} + + colorette@1.1.0: {} + + colorette@1.2.1: {} + + colorette@1.4.0: {} + + colorette@2.0.19: {} + + colorette@2.0.20: {} + + colors@1.0.3: {} + + colors@1.4.0: {} + + combine-errors@3.0.3: + dependencies: + custom-error-instance: 2.1.1 + lodash.uniqby: 4.5.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@0.6.1: {} + + commander@10.0.1: {} + + commander@11.1.0: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + commander@2.3.0: {} + + commander@2.8.1: + dependencies: + graceful-readlink: 1.0.1 + + commander@4.1.1: {} + + commander@5.1.0: {} + + commander@6.2.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + commander@9.5.0: {} + + common-ancestor-path@1.0.1: {} + + common-tags@1.8.2: {} + + commondir@1.0.1: {} + + compare-ver@2.0.2: {} + + component-emitter@1.3.1: {} + + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + concurrently@8.2.2: + dependencies: + chalk: 4.1.2 + date-fns: 2.30.0 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.3 + spawn-command: 0.0.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + condense-whitespace@2.0.0: {} + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + optional: true + + configstore@5.0.1: + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + + connect-slashes@1.4.0: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + console-browserify@1.2.0: {} + + console-control-strings@1.1.0: {} + + console-ui@3.1.2: + dependencies: + chalk: 2.4.2 + inquirer: 6.5.2 + json-stable-stringify: 1.3.0 + ora: 3.4.0 + through2: 3.0.2 + + consolidate@0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8): + dependencies: + bluebird: 3.7.2 + optionalDependencies: + babel-core: 6.26.3 + handlebars: 4.7.9 + lodash: 4.17.23 + underscore: 1.13.8 + + consolidate@1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.17.23)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): + optionalDependencies: + '@babel/core': 7.29.0 + handlebars: 4.7.9 + lodash: 4.17.23 + mustache: 4.2.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + underscore: 1.13.8 + + constants-browserify@1.0.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-tag@2.0.3: {} + + content-type@1.0.5: {} + + continuable-cache@0.3.1: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-session@2.1.1: + dependencies: + cookies: 0.9.1 + debug: 3.2.7 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + transitivePeerDependencies: + - supports-color + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cookiejar@2.1.4: {} + + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + copy-concurrently@1.0.5: + dependencies: + aproba: 1.2.0 + fs-write-stream-atomic: 1.0.10 + iferr: 0.1.5 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + + copy-dereference@1.0.0: {} + + copy-descriptor@0.1.1: {} + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.1 + + core-js@2.6.12: {} + + core-object@3.1.5: + dependencies: + chalk: 2.4.2 + + core-util-is@1.0.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@5.2.1: + dependencies: + import-fresh: 2.0.0 + is-directory: 0.3.1 + js-yaml: 3.14.2 + parse-json: 4.0.0 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + countries-and-timezones@3.8.0: {} + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.26.2 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + create-ecdh@4.0.4: + dependencies: + bn.js: 4.12.3 + elliptic: 6.6.1 + + create-error@0.3.1: {} + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + + create-jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + optional: true + + create-jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + crelt@1.0.6: {} + + cron-validate@1.4.5: + dependencies: + yup: 0.32.9 + + cross-fetch@4.1.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-browserify@3.12.1: + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.5 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + hash-base: 3.0.5 + inherits: 2.0.4 + pbkdf2: 3.1.5 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + + crypto-random-string@2.0.0: {} + + crypto@0.0.3: {} + + cson-parser@4.0.9: + dependencies: + coffeescript: 1.12.7 + optional: true + + css-blank-pseudo@3.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + css-color-names@0.0.4: {} + + css-declaration-sorter@4.0.1: + dependencies: + postcss: 7.0.39 + timsort: 0.3.0 + + css-declaration-sorter@7.3.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-functions-list@3.3.3: {} + + css-has-pseudo@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + css-line-break@1.1.1: + dependencies: + base64-arraybuffer: 0.2.0 + + css-loader@5.2.7(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + loader-utils: 2.0.4 + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + schema-utils: 3.3.0 + semver: 7.7.4 + webpack: 5.105.4(@swc/core@1.15.21) + + css-loader@6.11.0(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + semver: 7.7.4 + optionalDependencies: + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + + css-loader@6.11.0(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) + postcss-modules-scope: 3.2.1(postcss@8.5.6) + postcss-modules-values: 4.0.0(postcss@8.5.6) + postcss-value-parser: 4.2.0 + semver: 7.7.4 + optionalDependencies: + webpack: 5.105.4(@swc/core@1.15.21) + + css-prefers-color-scheme@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-select-base-adapter@0.1.1: {} + + css-select@1.2.0: + dependencies: + boolbase: 1.0.0 + css-what: 2.1.3 + domutils: 1.5.1 + nth-check: 1.0.2 + + css-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 3.4.2 + domutils: 1.7.0 + nth-check: 1.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@1.0.0-alpha.29: + dependencies: + mdn-data: 1.1.4 + source-map: 0.5.7 + + css-tree@1.0.0-alpha.33: + dependencies: + mdn-data: 2.0.4 + source-map: 0.5.7 + + css-tree@1.0.0-alpha.37: + dependencies: + mdn-data: 2.0.4 + source-map: 0.6.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@2.1.3: {} + + css-what@3.4.2: {} + + css-what@6.2.2: {} + + css.escape@1.5.1: {} + + cssdb@7.11.2: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@4.0.8: + dependencies: + css-declaration-sorter: 4.0.1 + cssnano-util-raw-cache: 4.0.1 + postcss: 7.0.39 + postcss-calc: 7.0.5 + postcss-colormin: 4.0.3 + postcss-convert-values: 4.0.1 + postcss-discard-comments: 4.0.2 + postcss-discard-duplicates: 4.0.2 + postcss-discard-empty: 4.0.1 + postcss-discard-overridden: 4.0.1 + postcss-merge-longhand: 4.0.11 + postcss-merge-rules: 4.0.3 + postcss-minify-font-values: 4.0.2 + postcss-minify-gradients: 4.0.2 + postcss-minify-params: 4.0.2 + postcss-minify-selectors: 4.0.2 + postcss-normalize-charset: 4.0.1 + postcss-normalize-display-values: 4.0.2 + postcss-normalize-positions: 4.0.2 + postcss-normalize-repeat-style: 4.0.2 + postcss-normalize-string: 4.0.2 + postcss-normalize-timing-functions: 4.0.2 + postcss-normalize-unicode: 4.0.1 + postcss-normalize-url: 4.0.1 + postcss-normalize-whitespace: 4.0.2 + postcss-ordered-values: 4.1.2 + postcss-reduce-initial: 4.0.3 + postcss-reduce-transforms: 4.0.2 + postcss-svgo: 4.0.3 + postcss-unique-selectors: 4.0.1 + + cssnano-preset-default@7.0.11(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + css-declaration-sorter: 7.3.1(postcss@8.5.6) + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 10.1.1(postcss@8.5.6) + postcss-colormin: 7.0.6(postcss@8.5.6) + postcss-convert-values: 7.0.9(postcss@8.5.6) + postcss-discard-comments: 7.0.6(postcss@8.5.6) + postcss-discard-duplicates: 7.0.2(postcss@8.5.6) + postcss-discard-empty: 7.0.1(postcss@8.5.6) + postcss-discard-overridden: 7.0.1(postcss@8.5.6) + postcss-merge-longhand: 7.0.5(postcss@8.5.6) + postcss-merge-rules: 7.0.8(postcss@8.5.6) + postcss-minify-font-values: 7.0.1(postcss@8.5.6) + postcss-minify-gradients: 7.0.1(postcss@8.5.6) + postcss-minify-params: 7.0.6(postcss@8.5.6) + postcss-minify-selectors: 7.0.6(postcss@8.5.6) + postcss-normalize-charset: 7.0.1(postcss@8.5.6) + postcss-normalize-display-values: 7.0.1(postcss@8.5.6) + postcss-normalize-positions: 7.0.1(postcss@8.5.6) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) + postcss-normalize-string: 7.0.1(postcss@8.5.6) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) + postcss-normalize-unicode: 7.0.6(postcss@8.5.6) + postcss-normalize-url: 7.0.1(postcss@8.5.6) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) + postcss-ordered-values: 7.0.2(postcss@8.5.6) + postcss-reduce-initial: 7.0.6(postcss@8.5.6) + postcss-reduce-transforms: 7.0.1(postcss@8.5.6) + postcss-svgo: 7.1.1(postcss@8.5.6) + postcss-unique-selectors: 7.0.5(postcss@8.5.6) + + cssnano-util-get-arguments@4.0.0: {} + + cssnano-util-get-match@4.0.0: {} + + cssnano-util-raw-cache@4.0.1: + dependencies: + postcss: 7.0.39 + + cssnano-util-same-parent@4.0.1: {} + + cssnano-utils@5.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + cssnano@4.1.10: + dependencies: + cosmiconfig: 5.2.1 + cssnano-preset-default: 4.0.8 + is-resolvable: 1.1.0 + postcss: 7.0.39 + + cssnano@7.1.1(postcss@8.5.6): + dependencies: + cssnano-preset-default: 7.0.11(postcss@8.5.6) + lilconfig: 3.1.3 + postcss: 8.5.6 + + csso@3.5.1: + dependencies: + css-tree: 1.0.0-alpha.29 + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + cssom@0.3.8: {} + + cssom@0.4.4: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.2.7 + + cssstyle@6.2.0: + dependencies: + '@asamuzakjp/css-color': 5.0.1 + '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.2.7 + + csstype@3.2.3: {} + + csv-writer@1.6.0: {} + + custom-error-instance@2.1.1: {} + + cyclist@1.0.2: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + dag-map@2.0.2: {} + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-uri-to-buffer@5.0.1: {} + + data-uri-utils@1.0.12: + dependencies: + data-uri-to-buffer: 5.0.1 + + data-urls@2.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + data-urls@7.0.0(@noble/hashes@1.8.0): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.29.2 + + date-format@4.0.14: {} + + date-time@2.1.0: + dependencies: + time-zone: 1.0.0 + + de-indent@1.0.2: {} + + debug-logfmt@1.4.10: + dependencies: + '@kikobeats/time-span': 1.0.12 + null-prototype-object: 1.2.6 + pretty-ms: 7.0.1 + + debug@2.2.0(supports-color@1.2.0): + dependencies: + ms: 0.7.1 + optionalDependencies: + supports-color: 1.2.0 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.1.1: + dependencies: + ms: 2.1.3 + + debug@4.3.1: + dependencies: + ms: 2.1.2 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decamelize@4.0.0: {} + + decamelize@5.0.1: {} + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + decode-uri-component@0.2.2: {} + + decompress-response@10.0.0: + dependencies: + mimic-response: 4.0.0 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + decorator-transforms@2.3.1(@babel/core@7.29.0): + dependencies: + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + babel-import-util: 3.0.1 + transitivePeerDependencies: + - '@babel/core' + + dedent@1.7.2(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-eql@5.0.2: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + deep-extend@0.6.0: + optional: true + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + deferred-leveldown@0.2.0: + dependencies: + abstract-leveldown: 0.12.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + denque@1.5.1: {} + + denque@2.1.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dependency-graph@1.0.0: {} + + dequal@2.0.3: {} + + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + destroy@1.2.0: {} + + detect-file@1.0.0: {} + + detect-indent@4.0.0: + dependencies: + repeating: 2.0.1 + + detect-indent@6.1.0: {} + + detect-libc@2.1.2: {} + + detect-newline@3.1.0: {} + + detect-node-es@1.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diacritics@1.3.0: {} + + didyoumean@1.2.2: {} + + diff-sequences@28.1.1: {} + + diff-sequences@29.6.3: {} + + diff@1.4.0: {} + + diff@4.0.4: {} + + diff@5.2.2: {} + + diff@7.0.0: {} + + diff@8.0.4: {} + + diffie-hellman@5.0.3: + dependencies: + bn.js: 4.12.3 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + discontinuous-range@1.0.0: {} + + dlv@1.1.3: {} + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.10: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.3 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.5.4 + tar-fs: 2.1.4 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + dom-serializer@0.1.1: + dependencies: + domelementtype: 1.3.1 + entities: 1.1.2 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domain-browser@1.2.0: {} + + domelementtype@1.3.1: {} + + domelementtype@2.3.0: {} + + domexception@2.0.1: + dependencies: + webidl-conversions: 5.0.0 + + domhandler@2.4.2: + dependencies: + domelementtype: 1.3.1 + + domhandler@3.3.0: + dependencies: + domelementtype: 2.3.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + domutils@1.5.1: + dependencies: + dom-serializer: 0.1.1 + domelementtype: 1.3.1 + + domutils@1.7.0: + dependencies: + dom-serializer: 0.1.1 + domelementtype: 1.3.1 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv-expand@10.0.0: {} + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.4.7: {} + + dotenv@16.6.1: {} + + dotenv@17.3.1: {} + + downsize@0.0.8: + dependencies: + xregexp: 2.0.0 + + dtrace-provider@0.8.8: + dependencies: + nan: 2.26.2 + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + eastasianwidth@0.2.0: {} + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + + editions@1.3.4: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.7.4 + optional: true + + ee-argv@0.1.4: {} + + ee-class@1.4.0: {} + + ee-first@1.1.1: {} + + ee-log@1.1.0: + dependencies: + ee-class: 1.4.0 + ee-types: 2.2.1 + + ee-log@3.0.9: + dependencies: + '@distributed-systems/callsite': 1.1.1 + logd-console-output: 1.3.0 + + ee-types@2.2.1: + dependencies: + ee-class: 1.4.0 + + electron-to-chromium@1.5.328: {} + + element-closest@2.0.2: {} + + element-resize-detector@1.2.4: + dependencies: + batch-processor: 1.0.0 + + elliptic@6.6.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + ember-ajax@5.1.2: + dependencies: + ember-cli-babel: 7.26.11 + najax: 1.0.7 + transitivePeerDependencies: + - supports-color + + ember-assign-helper@0.2.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-assign-helper@0.4.0: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + transitivePeerDependencies: + - supports-color + + ember-assign-helper@0.5.0(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/addon-shim': 1.10.2 + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + ember-assign-polyfill@2.7.3(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 8.2.0(@babel/core@7.29.0) + ember-cli-version-checker: 2.2.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-auto-import@1.12.2: + dependencies: + '@babel/core': 7.29.0 + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@embroider/shared-internals': 1.8.3 + babel-core: 6.26.3 + babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@4.47.0) + babel-plugin-syntax-dynamic-import: 6.18.0 + babylon: 6.18.0 + broccoli-debug: 0.6.5 + broccoli-node-api: 1.7.0 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + debug: 3.2.7 + ember-cli-babel: 7.26.11 + enhanced-resolve: 4.5.0 + fs-extra: 6.0.1 + fs-tree-diff: 2.0.1 + handlebars: 4.7.9 + js-string-escape: 1.0.1 + lodash: 4.17.23 + mkdirp: 0.5.6 + resolve-package-path: 3.1.0 + rimraf: 2.7.1 + semver: 7.7.4 + symlink-or-copy: 1.3.1 + typescript-memoize: 1.1.1 + walk-sync: 0.3.4 + webpack: 4.47.0 + transitivePeerDependencies: + - supports-color + - webpack-cli + - webpack-command + + ember-auto-import@2.10.0(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@embroider/macros': 1.16.13 + '@embroider/shared-internals': 2.9.2 + babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.105.4(@swc/core@1.15.21)) + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-ember-template-compilation: 2.4.1 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + babel-plugin-syntax-dynamic-import: 6.18.0 + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-plugin: 4.0.7 + broccoli-source: 3.0.1 + css-loader: 5.2.7(webpack@5.105.4(@swc/core@1.15.21)) + debug: 4.4.3(supports-color@5.5.0) + fs-extra: 10.1.0 + fs-tree-diff: 2.0.1 + handlebars: 4.7.8 + is-subdir: 1.2.0 + js-string-escape: 1.0.1 + lodash: 4.17.23 + mini-css-extract-plugin: 2.10.2(webpack@5.105.4(@swc/core@1.15.21)) + minimatch: 3.1.5 + parse5: 6.0.1 + pkg-entry-points: 1.1.1 + resolve: 1.22.11 + resolve-package-path: 4.0.3 + semver: 7.7.4 + style-loader: 2.0.0(webpack@5.105.4(@swc/core@1.15.21)) + typescript-memoize: 1.1.1 + walk-sync: 3.0.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + - webpack + + ember-basic-dropdown@6.0.2(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@ember/render-modifiers': 2.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + '@embroider/macros': 1.16.13 + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + '@glimmer/component': 1.1.2(@babel/core@7.29.0) + '@glimmer/tracking': 1.1.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-cli-typescript: 4.2.1 + ember-element-helper: 0.6.1(ember-source@3.24.0(@babel/core@7.29.0)) + ember-get-config: 2.1.1 + ember-maybe-in-element: 2.1.0 + ember-modifier: 3.2.7(@babel/core@7.29.0) + ember-style-modifier: 1.0.0(@babel/core@7.29.0) + ember-truth-helpers: 3.1.1 + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - supports-color + + ember-classic-decorator@3.0.1: + dependencies: + '@embroider/macros': 1.16.13 + babel-plugin-filter-imports: 4.0.0 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + + ember-cli-app-version@5.0.0: + dependencies: + ember-cli-babel: 7.26.11 + git-repo-info: 2.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-babel-plugin-helpers@1.1.1: {} + + ember-cli-babel@6.18.0(@babel/core@7.29.0): + dependencies: + amd-name-resolver: 1.2.0 + babel-plugin-debug-macros: 0.2.0(@babel/core@7.29.0) + babel-plugin-ember-modules-api-polyfill: 2.13.4 + babel-plugin-transform-es2015-modules-amd: 6.24.1 + babel-polyfill: 6.26.0 + babel-preset-env: 1.7.0 + broccoli-babel-transpiler: 6.5.1 + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-source: 1.1.0 + clone: 2.1.2 + ember-cli-version-checker: 2.2.0 + semver: 5.7.2 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-babel@7.26.11: + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/polyfill': 7.12.1 + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/runtime': 7.12.18 + amd-name-resolver: 1.3.1 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-ember-data-packages-polyfill: 0.1.2 + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-module-resolver: 3.2.0 + broccoli-babel-transpiler: 7.8.1 + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-source: 2.1.2 + calculate-cache-key-for-tree: 2.0.0 + clone: 2.1.2 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 4.1.1 + ensure-posix-path: 1.1.1 + fixturify-project: 1.10.0 + resolve-package-path: 3.1.0 + rimraf: 3.0.2 + semver: 5.7.2 + transitivePeerDependencies: + - supports-color + + ember-cli-babel@8.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/runtime': 7.12.18 + amd-name-resolver: 1.3.1 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-ember-data-packages-polyfill: 0.1.2 + babel-plugin-ember-modules-api-polyfill: 3.5.0 + babel-plugin-module-resolver: 5.0.3 + broccoli-babel-transpiler: 8.0.2(@babel/core@7.29.0) + broccoli-debug: 0.6.5 + broccoli-funnel: 3.0.8 + broccoli-source: 3.0.1 + calculate-cache-key-for-tree: 2.0.0 + clone: 2.1.2 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 + ensure-posix-path: 1.1.1 + resolve-package-path: 4.0.3 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + ember-cli-chart@3.7.2: + dependencies: + chart.js: 2.9.4 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-cli-node-assets: 0.2.2 + fastboot-transform: 0.1.3 + transitivePeerDependencies: + - supports-color + + ember-cli-code-coverage@1.0.3: + dependencies: + babel-plugin-istanbul: 6.1.1 + body-parser: 1.20.3 + ember-cli-version-checker: 5.1.2 + fs-extra: 9.1.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + node-dir: 0.1.17 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-dependency-checker@3.3.2(ember-cli@3.24.0(encoding@0.1.13)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8)): + dependencies: + chalk: 2.4.2 + ember-cli: 3.24.0(encoding@0.1.13)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + find-yarn-workspace-root: 1.2.1 + is-git-url: 1.0.0 + resolve: 1.22.11 + semver: 5.7.2 + transitivePeerDependencies: + - supports-color + + ember-cli-deprecation-workflow@2.2.0: + dependencies: + '@ember/string': 3.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-plugin: 4.0.7 + transitivePeerDependencies: + - supports-color + + ember-cli-element-closest-polyfill@0.0.1(@babel/core@7.29.0): + dependencies: + broccoli-funnel: 2.0.2 + caniuse-api: 3.0.0 + element-closest: 2.0.2 + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + fastboot-transform: 0.1.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-get-component-path-option@1.0.0: {} + + ember-cli-htmlbars-inline-precompile@2.1.0(ember-cli-babel@7.26.11): + dependencies: + babel-plugin-htmlbars-inline-precompile: 1.0.0 + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 2.2.0 + hash-for-dep: 1.5.2 + heimdalljs-logger: 0.1.10 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-htmlbars@3.1.0: + dependencies: + broccoli-persistent-filter: 2.3.1 + hash-for-dep: 1.5.2 + json-stable-stringify: 1.3.0 + strip-bom: 3.0.0 + transitivePeerDependencies: + - supports-color + + ember-cli-htmlbars@4.5.0: + dependencies: + '@ember/edition-utils': 1.2.0 + babel-plugin-htmlbars-inline-precompile: 3.2.0 + broccoli-debug: 0.6.5 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 3.1.0 + common-tags: 1.8.2 + ember-cli-babel-plugin-helpers: 1.1.1 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.2 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + semver: 6.3.1 + strip-bom: 4.0.0 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-htmlbars@5.7.2: + dependencies: + '@ember/edition-utils': 1.2.0 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + broccoli-debug: 0.6.5 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 4.0.7 + common-tags: 1.8.2 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.2 + heimdalljs-logger: 0.1.10 + json-stable-stringify: 1.3.0 + semver: 7.7.4 + silent-error: 1.1.1 + strip-bom: 4.0.0 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-htmlbars@6.3.0: + dependencies: + '@ember/edition-utils': 1.2.0 + babel-plugin-ember-template-compilation: 2.4.1 + babel-plugin-htmlbars-inline-precompile: 5.3.1 + broccoli-debug: 0.6.5 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 4.0.7 + ember-cli-version-checker: 5.1.2 + fs-tree-diff: 2.0.1 + hash-for-dep: 1.5.2 + heimdalljs-logger: 0.1.10 + js-string-escape: 1.0.1 + semver: 7.7.4 + silent-error: 1.1.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-inject-live-reload@2.1.0: + dependencies: + clean-base-url: 1.0.0 + ember-cli-version-checker: 3.1.3 + + ember-cli-is-package-missing@1.0.0: {} + + ember-cli-lodash-subset@2.0.1: {} + + ember-cli-mirage@2.4.0(@babel/core@7.29.0)(@ember/test-helpers@2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)))(ember-data@3.24.0(@babel/core@7.29.0))(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/macros': 0.41.0 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + ember-auto-import: 1.12.2 + ember-cli-babel: 7.26.11 + ember-destroyable-polyfill: 2.0.3(@babel/core@7.29.0) + ember-get-config: 0.5.0 + ember-inflector: 4.0.3(ember-source@3.24.0(@babel/core@7.29.0)) + lodash-es: 4.18.1 + miragejs: 0.1.48 + optionalDependencies: + '@ember/test-helpers': 2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-data: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - ember-source + - supports-color + - webpack-cli + - webpack-command + + ember-cli-node-assets@0.2.2: + dependencies: + broccoli-funnel: 1.2.0 + broccoli-merge-trees: 1.2.4 + broccoli-source: 1.1.0 + debug: 2.6.9 + lodash: 4.17.23 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + ember-cli-normalize-entity-name@1.0.0: + dependencies: + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-path-utils@1.0.0: {} + + ember-cli-postcss@6.0.1: + dependencies: + broccoli-file-creator: 2.1.1 + broccoli-merge-trees: 4.2.0 + broccoli-postcss: 5.1.0 + broccoli-postcss-single: 4.0.1 + ember-cli-babel: 7.26.11 + merge: 1.2.1 + transitivePeerDependencies: + - supports-color + + ember-cli-preprocess-registry@3.3.0: + dependencies: + broccoli-clean-css: 1.1.0 + broccoli-funnel: 2.0.2 + debug: 3.2.7 + process-relative-require: 1.0.0 + transitivePeerDependencies: + - supports-color + + ember-cli-shims@1.2.0: + dependencies: + broccoli-file-creator: 1.2.0 + broccoli-merge-trees: 2.0.1 + ember-cli-version-checker: 2.2.0 + ember-rfc176-data: 0.3.18 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-string-helpers@6.1.0: + dependencies: + '@babel/core': 7.29.0 + broccoli-funnel: 3.0.8 + ember-cli-babel: 7.26.11 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + ember-cli-string-utils@1.1.0: {} + + ember-cli-terser@4.0.1: + dependencies: + broccoli-terser-sourcemap: 4.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-test-info@1.0.0: + dependencies: + ember-cli-string-utils: 1.1.0 + + ember-cli-test-loader@2.2.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-test-loader@3.1.0: + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + ember-cli-typescript@2.0.2(@babel/core@7.29.0): + dependencies: + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.4.5(@babel/core@7.29.0) + ansi-to-html: 0.6.15 + debug: 4.4.3(supports-color@5.5.0) + ember-cli-babel-plugin-helpers: 1.1.1 + execa: 1.0.0 + fs-extra: 7.0.1 + resolve: 1.22.11 + rsvp: 4.8.5 + semver: 6.3.1 + stagehand: 1.0.1 + walk-sync: 1.1.4 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-typescript@3.0.0(@babel/core@7.29.0): + dependencies: + '@babel/plugin-transform-typescript': 7.5.5(@babel/core@7.29.0) + ansi-to-html: 0.6.15 + debug: 4.4.3(supports-color@5.5.0) + ember-cli-babel-plugin-helpers: 1.1.1 + execa: 2.1.0 + fs-extra: 8.1.0 + resolve: 1.22.11 + rsvp: 4.8.5 + semver: 6.3.1 + stagehand: 1.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-typescript@3.1.4(@babel/core@7.29.0): + dependencies: + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.8.7(@babel/core@7.29.0) + ansi-to-html: 0.6.15 + broccoli-stew: 3.0.0 + debug: 4.4.3(supports-color@5.5.0) + ember-cli-babel-plugin-helpers: 1.1.1 + execa: 3.4.0 + fs-extra: 8.1.0 + resolve: 1.22.11 + rsvp: 4.8.5 + semver: 6.3.1 + stagehand: 1.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cli-typescript@4.2.1: + dependencies: + ansi-to-html: 0.6.15 + broccoli-stew: 3.0.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 4.1.0 + fs-extra: 9.1.0 + resolve: 1.22.11 + rsvp: 4.8.5 + semver: 7.7.4 + stagehand: 1.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-typescript@5.3.0: + dependencies: + ansi-to-html: 0.6.15 + broccoli-stew: 3.0.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 4.1.0 + fs-extra: 9.1.0 + resolve: 1.22.11 + rsvp: 4.8.5 + semver: 7.7.4 + stagehand: 1.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-cli-version-checker@2.2.0: + dependencies: + resolve: 1.22.11 + semver: 5.7.2 + + ember-cli-version-checker@3.1.3: + dependencies: + resolve-package-path: 1.2.7 + semver: 5.7.2 + + ember-cli-version-checker@4.1.1: + dependencies: + resolve-package-path: 2.0.0 + semver: 6.3.1 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli-version-checker@5.1.2: + dependencies: + resolve-package-path: 3.1.0 + semver: 7.7.4 + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-cli@3.24.0(encoding@0.1.13)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + amd-name-resolver: 1.3.1 + babel-plugin-module-resolver: 4.1.0 + bower-config: 1.4.3 + bower-endpoint-parser: 0.2.2 + broccoli: 3.5.2 + broccoli-amd-funnel: 2.0.1 + broccoli-babel-transpiler: 7.8.1 + broccoli-builder: 0.18.14 + broccoli-concat: 4.2.7 + broccoli-config-loader: 1.0.1 + broccoli-config-replace: 1.1.3 + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-funnel-reducer: 1.0.0 + broccoli-merge-trees: 3.0.2 + broccoli-middleware: 2.1.1 + broccoli-slow-trees: 3.1.0 + broccoli-source: 3.0.1 + broccoli-stew: 3.0.0 + calculate-cache-key-for-tree: 2.0.0 + capture-exit: 2.0.0 + chalk: 4.1.2 + ci-info: 2.0.0 + clean-base-url: 1.0.0 + compression: 1.8.1 + configstore: 5.0.1 + console-ui: 3.1.2 + core-object: 3.1.5 + dag-map: 2.0.2 + diff: 4.0.4 + ember-cli-is-package-missing: 1.0.0 + ember-cli-lodash-subset: 2.0.1 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-preprocess-registry: 3.3.0 + ember-cli-string-utils: 1.1.0 + ember-source-channel-url: 3.0.0(encoding@0.1.13) + ensure-posix-path: 1.1.1 + execa: 4.1.0 + exit: 0.1.2 + express: 4.21.2 + filesize: 6.4.0 + find-up: 5.0.0 + find-yarn-workspace-root: 2.0.0 + fixturify-project: 2.1.1 + fs-extra: 9.1.0 + fs-tree-diff: 2.0.1 + get-caller-file: 2.0.5 + git-repo-info: 2.1.1 + glob: 7.2.3 + heimdalljs: 0.2.6 + heimdalljs-fs-monitor: 1.1.2 + heimdalljs-graph: 1.0.0 + heimdalljs-logger: 0.1.10 + http-proxy: 1.18.1 + inflection: 1.13.4 + is-git-url: 1.0.0 + is-language-code: 2.0.0 + isbinaryfile: 4.0.10 + js-yaml: 3.14.2 + json-stable-stringify: 1.3.0 + leek: 0.0.24 + lodash.template: 4.5.0 + markdown-it: 12.3.2 + markdown-it-terminal: 0.2.1 + minimatch: 3.1.5 + morgan: 1.10.1 + nopt: 3.0.6 + npm-package-arg: 8.1.5 + p-defer: 3.0.0 + portfinder: 1.0.38 + promise-map-series: 0.3.0 + promise.hash.helper: 1.0.8 + quick-temp: 0.1.9 + resolve: 1.22.11 + resolve-package-path: 3.1.0 + sane: 4.1.0 + semver: 7.7.4 + silent-error: 1.1.1 + sort-package-json: 1.57.0 + symlink-or-copy: 1.3.1 + temp: 0.9.4 + testem: 3.19.1(@babel/core@7.29.0)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + tiny-lr: 2.0.0 + tree-sync: 2.1.0 + uuid: 8.3.2 + walk-sync: 2.2.0 + watch-detector: 1.0.2 + workerpool: 6.5.1 + yam: 1.0.0 + transitivePeerDependencies: + - arc-templates + - atpl + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - encoding + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jazz + - jqtpl + - just + - liquid-node + - liquor + - mote + - nunjucks + - plates + - pug + - qejs + - ractive + - react + - react-dom + - slm + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - whiskers + + ember-compatibility-helpers@1.2.7(@babel/core@7.29.0): + dependencies: + babel-plugin-debug-macros: 0.2.0(@babel/core@7.29.0) + ember-cli-version-checker: 5.1.2 + find-up: 5.0.0 + fs-extra: 9.1.0 + semver: 5.7.2 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-composable-helpers@5.0.0: + dependencies: + '@babel/core': 7.29.0 + broccoli-funnel: 2.0.1 + ember-cli-babel: 7.26.11 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + ember-concurrency-decorators@2.0.3(@babel/core@7.29.0): + dependencies: + '@ember-decorators/utils': 6.1.1 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 4.5.0 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-concurrency@1.3.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + ember-maybe-import-regenerator: 0.1.6(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-concurrency@2.3.7(@babel/core@7.29.0): + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + '@glimmer/tracking': 1.1.2 + ember-cli-babel: 7.26.11 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-htmlbars: 5.7.2 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + ember-destroyable-polyfill: 2.0.3(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-cookies@0.5.2: + dependencies: + ember-cli-babel: 7.26.11 + ember-getowner-polyfill: 2.2.0 + transitivePeerDependencies: + - supports-color + + ember-could-get-used-to-this@1.0.1: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 4.5.0 + transitivePeerDependencies: + - supports-color + + ember-css-transitions@4.4.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@ember/test-waiters': 3.1.0 + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 2.3.1(@babel/core@7.29.0) + ember-modifier: 4.2.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + transitivePeerDependencies: + - '@babel/core' + - ember-source + - supports-color + + ember-data@3.24.0(@babel/core@7.29.0): + dependencies: + '@ember-data/adapter': 3.24.0(@babel/core@7.29.0) + '@ember-data/debug': 3.24.0(@babel/core@7.29.0) + '@ember-data/model': 3.24.0(@babel/core@7.29.0) + '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.0) + '@ember-data/record-data': 3.24.0(@babel/core@7.29.0) + '@ember-data/serializer': 3.24.0(@babel/core@7.29.0) + '@ember-data/store': 3.24.0(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@ember/ordered-set': 4.0.0(@babel/core@7.29.0) + '@ember/string': 1.1.0 + '@glimmer/env': 0.1.7 + broccoli-merge-trees: 4.2.0 + ember-cli-babel: 7.26.11 + ember-cli-typescript: 3.1.4(@babel/core@7.29.0) + ember-inflector: 3.0.1(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-decorators@6.1.1: + dependencies: + '@ember-decorators/component': 6.1.1 + '@ember-decorators/object': 6.1.1 + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + ember-destroyable-polyfill@2.0.3(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-drag-drop@0.4.8(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-element-helper@0.2.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-element-helper@0.6.1(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@glint/environment-ember-loose' + - '@glint/template' + - supports-color + + ember-ella-sparse@0.16.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-concurrency: 1.3.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-eslint-parser@0.5.13(@babel/core@7.29.0)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@babel/core': 7.29.0 + '@babel/eslint-parser': 7.28.4(@babel/core@7.29.0)(eslint@8.57.1) + '@glimmer/syntax': 0.95.0 + '@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3) + content-tag: 2.0.3 + eslint-scope: 7.2.2 + html-tags: 3.3.1 + mathml-tag-names: 2.1.3 + svg-tags: 1.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + transitivePeerDependencies: + - eslint + - typescript + + ember-exam@6.0.1(ember-mocha@0.16.2(@babel/core@7.29.0)): + dependencies: + '@embroider/macros': 0.29.0 + chalk: 4.1.2 + cli-table3: 0.6.5 + debug: 4.4.3(supports-color@5.5.0) + ember-cli-babel: 7.26.11 + execa: 4.1.0 + fs-extra: 9.1.0 + js-yaml: 3.14.2 + npmlog: 4.1.2 + rimraf: 3.0.2 + semver: 7.7.4 + silent-error: 1.1.1 + optionalDependencies: + ember-mocha: 0.16.2(@babel/core@7.29.0) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + ember-export-application-global@2.0.1: {} + + ember-factory-for-polyfill@1.3.1: + dependencies: + ember-cli-version-checker: 2.2.0 + + ember-fetch@8.1.2(encoding@0.1.13): + dependencies: + abortcontroller-polyfill: 1.7.8 + broccoli-concat: 4.2.7 + broccoli-debug: 0.6.5 + broccoli-merge-trees: 4.2.0 + broccoli-rollup: 2.1.1 + broccoli-stew: 3.0.0 + broccoli-templater: 2.0.2 + calculate-cache-key-for-tree: 2.0.0 + caniuse-api: 3.0.0 + ember-cli-babel: 7.26.11 + ember-cli-typescript: 4.2.1 + ember-cli-version-checker: 5.1.2 + node-fetch: 2.7.0(encoding@0.1.13) + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - encoding + - supports-color + + ember-get-config@0.5.0: + dependencies: + broccoli-file-creator: 1.2.0 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + transitivePeerDependencies: + - supports-color + + ember-get-config@2.1.1: + dependencies: + '@embroider/macros': 1.16.13 + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - '@glint/template' + - supports-color + + ember-getowner-polyfill@2.2.0: + dependencies: + ember-cli-version-checker: 2.2.0 + ember-factory-for-polyfill: 1.3.1 + + ember-in-element-polyfill@1.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-cli-version-checker: 5.1.2 + transitivePeerDependencies: + - supports-color + + ember-in-viewport@4.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + '@embroider/macros': 1.16.13 + ember-auto-import: 2.10.0(webpack@5.105.4(@swc/core@1.15.21)) + ember-cli-babel: 7.26.11 + ember-destroyable-polyfill: 2.0.3(@babel/core@7.29.0) + ember-modifier: 4.2.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + fast-deep-equal: 2.0.1 + intersection-observer-admin: 0.3.4 + raf-pool: 0.1.4 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - ember-source + - supports-color + - webpack + + ember-infinity@2.3.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + '@ember/render-modifiers': 2.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-in-viewport: 4.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(webpack@5.105.4(@swc/core@1.15.21)) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - ember-source + - supports-color + - webpack + + ember-inflector@3.0.1(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-inflector@4.0.3(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + ember-cli-babel: 7.26.11 + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + ember-keyboard@8.2.1(@babel/core@7.29.0)(@ember/test-helpers@2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)))(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/addon-shim': 1.10.2 + ember-destroyable-polyfill: 2.0.3(@babel/core@7.29.0) + ember-modifier: 4.2.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-modifier-manager-polyfill: 1.2.0(@babel/core@7.29.0) + optionalDependencies: + '@ember/test-helpers': 2.9.6(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + transitivePeerDependencies: + - '@babel/core' + - ember-source + - supports-color + + ember-load-initializers@2.1.2(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-typescript: 2.0.2(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-load@0.0.17: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 3.1.0 + ember-cli-version-checker: 3.1.3 + transitivePeerDependencies: + - supports-color + + ember-maybe-import-regenerator@0.1.6(@babel/core@7.29.0): + dependencies: + broccoli-funnel: 1.2.0 + broccoli-merge-trees: 1.2.4 + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + regenerator-runtime: 0.9.6 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-maybe-in-element@2.1.0: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-cli-version-checker: 5.1.2 + transitivePeerDependencies: + - supports-color + + ember-mocha@0.16.2(@babel/core@7.29.0): + dependencies: + '@ember/test-helpers': 1.7.3(@babel/core@7.29.0) + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + common-tags: 1.8.2 + ember-cli-babel: 7.26.11 + ember-cli-test-loader: 2.2.0(@babel/core@7.29.0) + mocha: 2.5.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-modifier-manager-polyfill@1.2.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 2.2.0 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-modifier@3.2.7(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 5.3.0 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-modifier@4.2.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 2.3.1(@babel/core@7.29.0) + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-string-utils: 1.1.0 + optionalDependencies: + ember-source: 3.24.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-moment@10.0.1(moment-timezone@0.5.45)(moment@2.24.0): + dependencies: + '@embroider/addon-shim': 1.10.2 + optionalDependencies: + moment: 2.24.0 + moment-timezone: 0.5.45 + transitivePeerDependencies: + - supports-color + + ember-one-way-select@4.0.1: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + transitivePeerDependencies: + - supports-color + + ember-power-calendar@0.15.0(@babel/core@7.29.0): + dependencies: + ember-assign-helper: 0.2.0(@babel/core@7.29.0) + ember-cli-babel: 7.26.11 + ember-cli-element-closest-polyfill: 0.0.1(@babel/core@7.29.0) + ember-cli-htmlbars: 4.5.0 + ember-concurrency: 1.3.0(@babel/core@7.29.0) + ember-decorators: 6.1.1 + ember-element-helper: 0.2.0(@babel/core@7.29.0) + ember-truth-helpers: 2.1.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-power-datepicker@0.8.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + ember-basic-dropdown: 6.0.2(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 4.5.0 + ember-decorators: 6.1.1 + ember-power-calendar: 0.15.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - supports-color + + ember-power-select@6.0.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + '@glimmer/component': 1.1.2(@babel/core@7.29.0) + '@glimmer/tracking': 1.1.2 + ember-assign-helper: 0.4.0 + ember-basic-dropdown: 6.0.2(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-cli-typescript: 4.2.1 + ember-concurrency: 2.3.7(@babel/core@7.29.0) + ember-concurrency-decorators: 2.0.3(@babel/core@7.29.0) + ember-text-measurer: 0.6.0 + ember-truth-helpers: 3.1.1 + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - supports-color + + ember-raf-scheduler@0.3.0: + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + ember-resolver@8.1.0(@babel/core@7.29.0): + dependencies: + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + resolve: 1.22.11 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-rfc176-data@0.3.18: {} + + ember-router-generator@2.0.0: + dependencies: + '@babel/parser': 7.29.2 + '@babel/traverse': 7.29.0 + recast: 0.18.10 + transitivePeerDependencies: + - supports-color + + ember-simple-auth@5.0.0(ember-fetch@8.1.2(encoding@0.1.13)): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-is-package-missing: 1.0.0 + ember-cookies: 0.5.2 + ember-fetch: 8.1.2(encoding@0.1.13) + silent-error: 1.1.1 + transitivePeerDependencies: + - supports-color + + ember-sinon@5.0.0: + dependencies: + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 3.0.2 + ember-cli-babel: 7.26.11 + sinon: 9.2.4 + transitivePeerDependencies: + - supports-color + + ember-source-channel-url@3.0.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + ember-source@3.24.0(@babel/core@7.29.0): + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-assign': 7.27.1(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-filter-imports: 4.0.0 + broccoli-concat: 4.2.7 + broccoli-debug: 0.6.5 + broccoli-funnel: 2.0.2 + broccoli-merge-trees: 4.2.0 + chalk: 4.1.2 + ember-cli-babel: 7.26.11 + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-version-checker: 5.1.2 + ember-router-generator: 2.0.0 + inflection: 1.13.4 + jquery: 3.7.1 + resolve: 1.22.11 + semver: 6.3.1 + silent-error: 1.1.1 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-style-modifier@1.0.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 7.26.11 + ember-modifier: 3.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-svg-jar@2.7.1: + dependencies: + '@embroider/macros': 1.16.13 + broccoli-caching-writer: 3.1.0 + broccoli-concat: 4.2.7 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-persistent-filter: 2.3.1 + broccoli-plugin: 4.0.7 + broccoli-replace: 2.0.2 + broccoli-svg-optimizer: 2.1.0 + cheerio: 1.0.0-rc.12 + console-ui: 3.1.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + lodash: 4.17.23 + safe-stable-stringify: 2.5.0 + transitivePeerDependencies: + - '@glint/template' + - supports-color + + ember-template-imports@3.4.2: + dependencies: + babel-import-util: 0.2.0 + broccoli-stew: 3.0.0 + ember-cli-babel-plugin-helpers: 1.1.1 + ember-cli-version-checker: 5.1.2 + line-column: 1.0.2 + magic-string: 0.25.9 + parse-static-imports: 1.1.0 + string.prototype.matchall: 4.0.12 + validate-peer-dependencies: 1.2.0 + transitivePeerDependencies: + - supports-color + + ember-template-lint@5.13.0: + dependencies: + '@lint-todo/utils': 13.1.1 + aria-query: 5.3.2 + chalk: 5.6.2 + ci-info: 3.9.0 + date-fns: 2.30.0 + ember-template-imports: 3.4.2 + ember-template-recast: 6.1.5 + eslint-formatter-kakoune: 1.0.0 + find-up: 6.3.0 + fuse.js: 6.6.2 + get-stdin: 9.0.0 + globby: 13.2.2 + is-glob: 4.0.3 + language-tags: 1.0.9 + micromatch: 4.0.8 + resolve: 1.22.11 + v8-compile-cache: 2.4.0 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + ember-template-recast@6.1.5: + dependencies: + '@glimmer/reference': 0.84.3 + '@glimmer/syntax': 0.84.3 + '@glimmer/validator': 0.84.3 + async-promise-queue: 1.0.5 + colors: 1.4.0 + commander: 8.3.0 + globby: 11.1.0 + ora: 5.4.1 + slash: 3.0.0 + tmp: 0.2.5 + workerpool: 6.5.1 + transitivePeerDependencies: + - supports-color + + ember-test-selectors@6.0.0: + dependencies: + calculate-cache-key-for-tree: 2.0.0 + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + transitivePeerDependencies: + - supports-color + + ember-test-waiters@1.2.0: + dependencies: + ember-cli-babel: 7.26.11 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + ember-text-measurer@0.6.0: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 4.5.0 + transitivePeerDependencies: + - supports-color + + ember-tooltips@3.6.0: + dependencies: + '@embroider/macros': 1.16.13 + broccoli-file-creator: 2.1.1 + broccoli-merge-trees: 4.2.0 + ember-auto-import: 1.12.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + ember-in-element-polyfill: 1.0.1 + popper.js: 1.16.1 + resolve: 1.22.11 + tooltip.js: 1.3.3 + transitivePeerDependencies: + - '@glint/template' + - supports-color + - webpack-cli + - webpack-command + + ember-tracked-storage-polyfill@1.0.0: + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 5.7.2 + transitivePeerDependencies: + - supports-color + + ember-truth-helpers@2.1.0(@babel/core@7.29.0): + dependencies: + ember-cli-babel: 6.18.0(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + ember-truth-helpers@3.1.1: + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + emittery@0.13.1: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojis-list@3.0.0: {} + + empathic@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-parser@5.2.3: {} + + engine.io@6.6.6: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 25.6.0 + '@types/ws': 8.18.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@4.5.0: + dependencies: + graceful-fs: 4.2.11 + memory-fs: 0.5.0 + tapable: 1.1.3 + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + ensure-posix-path@1.1.1: {} + + entities@1.1.2: {} + + entities@2.1.0: {} + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + eol@0.9.1: {} + + err-code@2.0.3: + optional: true + + errno@0.1.8: + dependencies: + prr: 1.0.1 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error@7.2.1: + dependencies: + string-template: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-array-method-boxes-properly@1.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-iterator-helpers@1.3.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + safe-array-concat: 1.1.3 + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es6-promise@4.2.8: {} + + esbuild-register@3.6.0(esbuild@0.18.20): + dependencies: + debug: 4.4.3(supports-color@5.5.0) + esbuild: 0.18.20 + transitivePeerDependencies: + - supports-color + + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3(supports-color@5.5.0) + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.20.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + escalade@3.2.0: {} + + escape-goat@3.0.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.2: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-compat-utils@0.5.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + semver: 7.7.4 + + eslint-formatter-kakoune@1.0.0: {} + + eslint-plugin-babel@5.3.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-rule-composer: 0.3.0 + + eslint-plugin-ember@12.7.5(@babel/core@7.29.0)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@ember-data/rfc395-data': 0.0.4 + css-tree: 3.2.1 + ember-eslint-parser: 0.5.13(@babel/core@7.29.0)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + ember-rfc176-data: 0.3.18 + eslint: 8.57.1 + eslint-utils: 3.0.0(eslint@8.57.1) + estraverse: 5.3.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + requireindex: 1.2.0 + snake-case: 3.0.4 + optionalDependencies: + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + transitivePeerDependencies: + - '@babel/core' + - typescript + + eslint-plugin-es-x@7.8.0(eslint@8.57.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) + + eslint-plugin-filenames-ts@1.3.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.upperfirst: 4.3.1 + + eslint-plugin-ghost@3.5.0(@babel/core@7.29.0)(eslint@8.57.1): + dependencies: + '@kapouer/eslint-plugin-no-return-in-loop': 1.0.0 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-plugin-ember: 12.7.5(@babel/core@7.29.0)(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-filenames-ts: 1.3.2(eslint@8.57.1) + eslint-plugin-mocha: 7.0.1(eslint@8.57.1) + eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-sort-imports-es6-autofix: 0.6.0(eslint@8.57.1) + eslint-plugin-unicorn: 42.0.0(eslint@8.57.1) + typescript: 5.9.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + eslint-plugin-i18next@6.1.3: + dependencies: + lodash: 4.17.21 + requireindex: 1.1.0 + + eslint-plugin-mocha@7.0.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-utils: 2.1.0 + ramda: 0.27.2 + + eslint-plugin-n@17.24.0(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + enhanced-resolve: 5.20.1 + eslint: 8.57.1 + eslint-plugin-es-x: 7.8.0(eslint@8.57.1) + get-tsconfig: 4.13.7 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.7.4 + ts-declaration-location: 1.0.7(typescript@5.9.3) + transitivePeerDependencies: + - typescript + + eslint-plugin-no-relative-import-paths@1.6.1: {} + + eslint-plugin-playwright@2.10.1(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + globals: 17.4.0 + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + + eslint-plugin-react-refresh@0.4.24(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react-refresh@0.4.24(eslint@9.37.0(jiti@2.6.1)): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.1 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-sort-imports-es6-autofix@0.6.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-storybook@10.3.3(eslint@8.57.1)(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.49.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + storybook: 10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + fast-glob: 3.3.3 + postcss: 8.5.6 + tailwindcss: 3.4.18(tsx@4.21.0)(yaml@2.8.3) + + eslint-plugin-tailwindcss@4.0.0-beta.0(tailwindcss@4.2.1): + dependencies: + fast-glob: 3.3.3 + postcss: 8.5.6 + synckit: 0.11.12 + tailwind-api-utils: 1.0.3(tailwindcss@4.2.1) + tailwindcss: 4.2.1 + + eslint-plugin-tailwindcss@4.0.0-beta.0(tailwindcss@4.2.2): + dependencies: + fast-glob: 3.3.3 + postcss: 8.5.6 + synckit: 0.11.12 + tailwind-api-utils: 1.0.3(tailwindcss@4.2.2) + tailwindcss: 4.2.2 + + eslint-plugin-unicorn@42.0.0(eslint@8.57.1): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + ci-info: 3.9.0 + clean-regexp: 1.0.0 + eslint: 8.57.1 + eslint-utils: 3.0.0(eslint@8.57.1) + esquery: 1.7.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + lodash: 4.17.23 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + safe-regex: 2.1.1 + semver: 7.7.4 + strip-indent: 3.0.0 + + eslint-rule-composer@0.3.0: {} + + eslint-scope@4.0.3: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-utils@3.0.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@5.5.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@9.37.0(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.37.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.37.0 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + esm-env@1.2.2: {} + + esm@3.2.25: {} + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + + esprima@3.0.0: {} + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@0.6.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + events-to-array@1.1.2: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + evp_bytestokey@1.0.3: + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + + exec-sh@0.3.6: {} + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@2.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 3.1.0 + onetime: 5.1.2 + p-finally: 2.0.1 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@3.4.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + p-finally: 2.0.1 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + exists-sync@0.0.4: {} + + exit@0.1.2: {} + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expand-template@2.0.3: + optional: true + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.3.0: {} + + expect@28.1.3: + dependencies: + '@jest/expect-utils': 28.1.3 + jest-get-type: 28.0.2 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + exponential-backoff@3.1.3: {} + + express-brute@1.0.1(express@4.21.2): + dependencies: + express: 4.21.2 + long-timeout: 0.1.1 + underscore: 1.8.3 + + express-end@0.0.8: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + express-handlebars@7.1.3: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + handlebars: 4.7.9 + + express-hbs@2.5.0: + dependencies: + handlebars: 4.7.9 + lodash: 4.17.23 + readdirp: 3.6.0 + optionalDependencies: + js-beautify: 1.15.4 + + express-jwt@8.5.1: + dependencies: + '@types/jsonwebtoken': 9.0.10 + express-unless: 2.1.3 + jsonwebtoken: 9.0.3 + + express-lazy-router@1.0.6(express@4.21.2): + dependencies: + express: 4.21.2 + + express-query-boolean@2.0.0: {} + + express-queue@0.0.13: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + express-end: 0.0.8 + mini-queue: 0.0.14 + transitivePeerDependencies: + - supports-color + + express-session@1.19.0: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.1.0 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express-unless@2.1.3: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extend@3.0.2: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + extract-stack@2.0.0: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fake-xml-http-request@2.1.2: {} + + fast-deep-equal@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@5.4.0: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-ordered-set@1.0.3: + dependencies: + blank-object: 1.0.2 + + fast-safe-stringify@2.1.1: {} + + fast-sourcemap-concat@1.4.0: + dependencies: + chalk: 2.4.2 + fs-extra: 5.0.0 + heimdalljs-logger: 0.1.10 + memory-streams: 0.1.3 + mkdirp: 0.5.6 + source-map: 0.4.4 + source-map-url: 0.3.0 + sourcemap-validator: 1.1.1 + transitivePeerDependencies: + - supports-color + + fast-sourcemap-concat@2.1.1: + dependencies: + chalk: 2.4.2 + fs-extra: 5.0.0 + heimdalljs-logger: 0.1.10 + memory-streams: 0.1.3 + mkdirp: 0.5.6 + source-map: 0.4.4 + source-map-url: 0.3.0 + transitivePeerDependencies: + - supports-color + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.2.0 + + fast-xml-parser@5.5.8: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.2.0 + strnum: 2.2.2 + + fastboot-transform@0.1.3: + dependencies: + broccoli-stew: 1.6.0 + convert-source-map: 1.9.0 + transitivePeerDependencies: + - supports-color + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.8.2: {} + + figgy-pudding@3.5.2: {} + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@7.0.2: + dependencies: + flat-cache: 3.2.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-extension@4.0.5: {} + + file-selector@0.6.0: + dependencies: + tslib: 2.8.1 + + file-system-cache@2.3.0: + dependencies: + fs-extra: 11.1.1 + ramda: 0.29.0 + + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + + file-uri-to-path@1.0.0: + optional: true + + filesize@4.2.1: {} + + filesize@6.4.0: {} + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-babel-config@1.2.2: + dependencies: + json5: 1.0.2 + path-exists: 3.0.0 + + find-babel-config@2.1.2: + dependencies: + json5: 2.2.3 + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-cache-dir@3.3.2: + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + find-index@1.1.1: {} + + find-root@1.1.0: {} + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + find-yarn-workspace-root@1.2.1: + dependencies: + fs-extra: 4.0.3 + micromatch: 3.1.10 + transitivePeerDependencies: + - supports-color + + find-yarn-workspace-root@2.0.0: + dependencies: + micromatch: 4.0.8 + + findup-sync@3.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 3.1.10 + resolve-dir: 1.0.1 + transitivePeerDependencies: + - supports-color + + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + fined@1.2.0: + dependencies: + expand-tilde: 2.0.2 + is-plain-object: 2.0.4 + object.defaults: 1.1.0 + object.pick: 1.3.0 + parse-filepath: 1.0.2 + + fireworm@0.7.2: + dependencies: + async: 0.2.10 + is-type: 0.0.1 + lodash.debounce: 3.1.1 + lodash.flatten: 3.0.2 + minimatch: 3.1.5 + + fixturify-project@1.10.0: + dependencies: + fixturify: 1.3.0 + tmp: 0.0.33 + + fixturify-project@2.1.1: + dependencies: + fixturify: 2.1.1 + tmp: 0.0.33 + type-fest: 0.11.0 + + fixturify@1.3.0: + dependencies: + '@types/fs-extra': 5.1.0 + '@types/minimatch': 3.0.5 + '@types/rimraf': 2.0.5 + fs-extra: 7.0.1 + matcher-collection: 2.0.1 + + fixturify@2.1.1: + dependencies: + '@types/fs-extra': 8.1.5 + '@types/minimatch': 3.0.5 + '@types/rimraf': 2.0.5 + fs-extra: 8.1.0 + matcher-collection: 2.0.1 + walk-sync: 2.2.0 + + flagged-respawn@1.0.1: {} + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.4.2: {} + + flatten@1.0.3: {} + + flexsearch@0.7.43: {} + + flexsearch@0.8.153: {} + + flush-write-stream@1.1.1: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + focus-trap@6.9.4: + dependencies: + tabbable: 5.3.3 + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + for-own@1.0.0: + dependencies: + for-in: 1.0.2 + + foreach@2.0.6: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forever-agent@0.6.1: {} + + form-data-encoder@2.1.4: {} + + form-data-encoder@4.1.0: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@3.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@1.2.6: {} + + formidable@2.1.5: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + qs: 6.15.0 + + forwarded-parse@2.1.2: {} + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.2 + + fs-constants@1.0.0: {} + + fs-extra@0.24.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@4.0.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@5.0.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@6.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-merger@3.2.1: + dependencies: + broccoli-node-api: 1.7.0 + broccoli-node-info: 2.2.0 + fs-extra: 8.1.0 + fs-tree-diff: 2.0.1 + walk-sync: 2.2.0 + transitivePeerDependencies: + - supports-color + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + optional: true + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.3 + + fs-mkdirp-stream@2.0.1: + dependencies: + graceful-fs: 4.2.11 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + fs-tree-diff@0.5.9: + dependencies: + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + fs-tree-diff@2.0.1: + dependencies: + '@types/symlink-or-copy': 1.2.2 + heimdalljs-logger: 0.1.10 + object-assign: 4.1.1 + path-posix: 1.0.0 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + fs-updater@1.0.4: + dependencies: + can-symlink: 1.0.0 + clean-up-path: 1.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + rimraf: 2.7.1 + transitivePeerDependencies: + - supports-color + + fs-write-stream-atomic@1.0.10: + dependencies: + graceful-fs: 4.2.11 + iferr: 0.1.5 + imurmurhash: 0.1.4 + readable-stream: 2.3.8 + + fs.realpath@1.0.0: {} + + fsevents@1.2.13: + dependencies: + bindings: 1.5.0 + nan: 2.26.2 + optional: true + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuse.js@6.6.2: {} + + fwd-stream@1.0.4: + dependencies: + readable-stream: 1.0.34 + + gauge@2.7.4: + dependencies: + aproba: 1.2.0 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 1.0.2 + strip-ansi: 3.0.1 + wide-align: 1.1.5 + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + gelf-stream@1.1.1: + dependencies: + gelfling: 0.3.1 + + gelfling@0.3.1: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stdin@4.0.1: {} + + get-stdin@9.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-value@2.0.6: {} + + getopts@2.2.5: {} + + getopts@2.3.0: {} + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + ghost-storage-base@1.1.2: + dependencies: + moment: 2.24.0 + + git-hooks-list@1.0.3: {} + + git-repo-info@2.1.1: {} + + github-from-package@0.0.0: + optional: true + + glob-parent@3.1.0: + dependencies: + is-glob: 3.1.0 + path-dirname: 1.0.2 + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-stream@8.0.3: + dependencies: + '@gulpjs/to-absolute-glob': 4.0.0 + anymatch: 3.1.3 + fastq: 1.20.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + is-negated-glob: 1.0.0 + normalize-path: 3.0.0 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 2.3.6 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.4 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@3.2.11: + dependencies: + inherits: 2.0.4 + minimatch: 0.3.0 + + glob@5.0.15: + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@6.0.4: + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.7 + minipass: 4.2.8 + path-scurry: 1.11.1 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globals@17.4.0: {} + + globals@9.18.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globalyzer@0.1.0: {} + + globby@10.0.0: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + + globjoin@0.1.4: {} + + globrex@0.1.2: {} + + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + + google-caja-bower@https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd: {} + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + got@13.0.0: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + + got@14.6.6: + dependencies: + '@sindresorhus/is': 7.2.0 + byte-counter: 0.1.0 + cacheable-lookup: 7.0.0 + cacheable-request: 13.0.18 + decompress-response: 10.0.0 + form-data-encoder: 4.1.0 + http2-wrapper: 2.2.1 + keyv: 5.6.0 + lowercase-keys: 3.0.0 + p-cancelable: 4.0.1 + responselike: 4.0.2 + type-fest: 4.41.0 + + graceful-fs@4.2.11: {} + + graceful-readlink@1.0.1: {} + + graphemer@1.4.0: {} + + graphql@16.13.2: {} + + growl@1.9.2: {} + + growly@1.3.0: {} + + gscan@5.4.3: + dependencies: + '@sentry/node': 10.47.0 + '@tryghost/config': 2.0.3 + '@tryghost/debug': 2.0.3 + '@tryghost/errors': 1.3.13 + '@tryghost/logging': 2.5.5 + '@tryghost/nql': 0.12.10 + '@tryghost/pretty-cli': 3.0.3 + '@tryghost/server': 2.0.3 + '@tryghost/zip': 3.0.3 + chalk: 5.6.2 + express: 5.2.1 + express-handlebars: 7.1.3 + glob: 13.0.6 + lodash: 4.18.1 + multer: 2.1.1 + semver: 7.7.4 + validator: 13.12.0 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + gulp-sort@2.0.0: + dependencies: + through2: 2.0.5 + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.14.0 + har-schema: 2.0.0 + + hard-rejection@2.1.0: {} + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-ansi@3.0.0: + dependencies: + ansi-regex: 3.0.1 + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + has-values@2.0.1: + dependencies: + kind-of: 6.0.3 + + has@1.0.4: {} + + hash-base@3.0.5: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + hash-for-dep@1.5.2: + dependencies: + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + resolve: 1.22.11 + resolve-package-path: 1.2.7 + transitivePeerDependencies: + - supports-color + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + headers-polyfill@4.0.3: {} + + heic-convert@2.1.0: + dependencies: + heic-decode: 2.1.0 + jpeg-js: 0.4.4 + pngjs: 6.0.0 + + heic-decode@2.1.0: + dependencies: + libheif-js: 1.19.8 + + heimdalljs-fs-monitor@1.1.2: + dependencies: + callsites: 3.1.0 + clean-stack: 2.2.0 + extract-stack: 2.0.0 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + transitivePeerDependencies: + - supports-color + + heimdalljs-graph@1.0.0: {} + + heimdalljs-logger@0.1.10: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color + + heimdalljs@0.2.6: + dependencies: + rsvp: 3.2.1 + + heimdalljs@0.3.3: + dependencies: + rsvp: 3.2.1 + + hex-color-regex@1.1.0: {} + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + home-or-tmp@2.0.0: + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hpagent@1.2.0: {} + + hsl-regex@1.0.0: {} + + hsla-regex@1.0.0: {} + + html-encoding-sniffer@2.0.1: + dependencies: + whatwg-encoding: 1.0.5 + + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + html-minifier@4.0.0: + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.19.3 + + html-tags@3.3.1: {} + + html-to-text@5.1.1: + dependencies: + he: 1.2.0 + htmlparser2: 3.10.1 + lodash: 4.17.23 + minimist: 1.2.8 + + html-to-text@8.2.1: + dependencies: + '@selderee/plugin-htmlparser2': 0.6.0 + deepmerge: 4.3.1 + he: 1.2.0 + htmlparser2: 6.1.0 + minimist: 1.2.8 + selderee: 0.6.0 + + html-validate@8.29.0(jest-diff@29.7.0)(jest-snapshot@29.7.0)(jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)))(vitest@1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + '@html-validate/stylish': 4.3.0 + '@sidvind/better-ajv-errors': 3.0.1(ajv@8.18.0) + ajv: 8.18.0 + glob: 10.5.0 + kleur: 4.1.5 + minimist: 1.2.8 + prompts: 2.4.2 + semver: 7.7.4 + optionalDependencies: + jest: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + jest-diff: 29.7.0 + jest-snapshot: 29.7.0 + vitest: 1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + html2canvas-objectfit-fix@1.2.0: + dependencies: + css-line-break: 1.1.1 + + html5parser@2.0.2: + dependencies: + tslib: 2.8.1 + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + htmlparser2@3.10.1: + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + + htmlparser2@5.0.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: {} + + http-errors@1.4.0: + dependencies: + inherits: 2.0.1 + statuses: 1.5.0 + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.11 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + httpntlm@1.6.1: + dependencies: + httpreq: 1.1.1 + underscore: 1.7.0 + + httpreq@1.1.1: {} + + https-browserify@1.0.0: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https@1.0.0: {} + + human-interval@2.0.1: + dependencies: + numbered: 1.1.0 + + human-number@2.0.10: {} + + human-signals@1.1.1: {} + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + human-signals@8.0.1: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + + husky@9.1.7: {} + + i18n-iso-countries@7.14.0: + dependencies: + diacritics: 1.3.0 + + i18next-parser@8.13.0: + dependencies: + '@babel/runtime': 7.29.2 + broccoli-plugin: 4.0.7 + cheerio: 1.0.0-rc.12 + colors: 1.4.0 + commander: 11.1.0 + eol: 0.9.1 + esbuild: 0.20.2 + fs-extra: 11.3.4 + gulp-sort: 2.0.0 + i18next: 23.16.8 + js-yaml: 4.1.0 + lilconfig: 3.1.3 + rsvp: 4.8.5 + sort-keys: 5.1.0 + typescript: 5.9.3 + vinyl: 3.0.1 + vinyl-fs: 4.0.2 + vue-template-compiler: 2.7.16 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + - supports-color + + i18next@23.16.8: + dependencies: + '@babel/runtime': 7.29.2 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + idb-wrapper@1.7.2: {} + + ieee754@1.2.1: {} + + iferr@0.1.5: {} + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-extensions@1.1.0: {} + + image-size@0.5.5: + optional: true + + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + immediate@3.0.6: {} + + import-fresh@2.0.0: + dependencies: + caller-path: 2.0.0 + resolve-from: 3.0.0 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-lazy@4.0.0: {} + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + include-path-searcher@0.1.0: {} + + indent-string@4.0.0: {} + + indent-string@5.0.0: {} + + indexes-of@1.0.1: {} + + indexof@0.0.1: {} + + infer-owner@1.0.4: {} + + inflected@2.1.0: {} + + inflection@1.12.0: {} + + inflection@1.13.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.1: {} + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + inline-source-map-comment@1.0.5: + dependencies: + chalk: 1.1.3 + get-stdin: 4.0.1 + minimist: 1.2.8 + sum-up: 1.0.3 + xtend: 4.0.2 + + inquirer@6.5.2: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 3.1.0 + figures: 2.0.0 + lodash: 4.17.23 + mute-stream: 0.0.7 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 2.1.1 + strip-ansi: 5.2.0 + through: 2.3.8 + + inquirer@7.3.3: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.23 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + + inquirer@8.2.7(@types/node@22.19.17): + dependencies: + '@inquirer/external-editor': 1.0.3(@types/node@22.19.17) + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + figures: 3.2.0 + lodash: 4.17.23 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' + + inquirer@8.2.7(@types/node@25.6.0): + dependencies: + '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + figures: 3.2.0 + lodash: 4.17.23 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' + + install-artifact-from-github@1.4.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@2.0.3: {} + + interpret@2.2.0: {} + + intersection-observer-admin@0.3.4: {} + + intl-format-cache@4.3.1: {} + + intl-messageformat-parser@2.1.3: {} + + intl-messageformat@5.4.3: + dependencies: + intl-format-cache: 4.3.1 + intl-messageformat-parser: 2.1.3 + + intl@1.2.5: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ioredis@4.31.0: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3(supports-color@5.5.0) + denque: 1.5.1 + lodash.defaults: 4.2.0 + lodash.flatten: 4.4.0 + lodash.isarguments: 3.1.0 + p-map: 2.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@10.1.0: {} + + ip-regex@4.3.0: {} + + ipaddr.js@1.9.1: {} + + is-absolute-url@2.1.0: {} + + is-absolute-url@3.0.3: {} + + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-alphabetical@1.0.4: {} + + is-alphanumeric@1.0.0: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@1.0.1: + dependencies: + binary-extensions: 1.13.1 + optional: true + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-buffer@2.0.5: {} + + is-builtin-module@3.2.1: + dependencies: + builtin-modules: 3.3.0 + + is-callable@1.2.7: {} + + is-color-stop@1.1.0: + dependencies: + css-color-names: 0.0.4 + hex-color-regex: 1.1.0 + hsl-regex: 1.0.0 + hsla-regex: 1.0.0 + rgb-regex: 1.0.1 + rgba-regex: 1.0.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@1.0.4: {} + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-directory@0.3.1: {} + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@1.0.0: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-finite@1.1.0: {} + + is-fullwidth-code-point@1.0.0: + dependencies: + number-is-nan: 1.0.1 + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-git-url@1.0.0: {} + + is-glob@2.0.1: + dependencies: + is-extglob: 1.0.0 + + is-glob@3.1.0: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@1.0.4: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@1.0.0: {} + + is-invalid-path@0.1.0: + dependencies: + is-glob: 2.0.1 + + is-lambda@1.0.1: + optional: true + + is-language-code@2.0.0: {} + + is-map@2.0.3: {} + + is-negated-glob@1.0.0: {} + + is-negative-zero@2.0.3: {} + + is-node-process@1.2.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-object@0.1.2: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@1.1.0: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-relative-url@3.0.0: + dependencies: + is-absolute-url: 3.0.3 + + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + + is-resolvable@1.1.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-stream@4.0.1: {} + + is-string-and-not-blank@0.0.2: + dependencies: + is-string-blank: 1.0.1 + + is-string-blank@1.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-type@0.0.1: + dependencies: + core-util-is: 1.0.3 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-typedarray@1.0.0: {} + + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-uri@1.2.13: + dependencies: + parse-uri: 2.0.5 + punycode2: 1.0.1 + + is-url-superb@4.0.0: {} + + is-valid-glob@1.0.0: {} + + is-valid-path@0.1.1: + dependencies: + is-invalid-path: 0.1.0 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-what@4.1.16: {} + + is-whitespace-character@1.0.4: {} + + is-windows@1.0.2: {} + + is-word-character@1.0.4: {} + + is-wsl@1.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + is@0.2.7: {} + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isbinaryfile@4.0.10: {} + + isbuffer@0.0.0: {} + + isexe@2.0.0: {} + + isexe@4.0.0: {} + + iso-639-3@2.2.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + isostring@0.0.1: {} + + isstream@0.1.2: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + istextorbinary@2.1.0: + dependencies: + binaryextensions: 2.3.0 + editions: 1.3.4 + textextensions: 2.6.0 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jade@0.26.3: + dependencies: + commander: 0.6.1 + mkdirp: 0.3.0 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0(babel-plugin-macros@3.1.0): + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + optional: true + + jest-cli@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.17 + ts-node: 10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.6.0 + ts-node: 10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0(babel-plugin-macros@3.1.0) + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 25.6.0 + ts-node: 10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@28.1.3: + dependencies: + chalk: 4.1.2 + diff-sequences: 28.1.1 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-extended@7.0.0(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + jest-diff: 30.3.0 + typescript: 5.9.3 + optionalDependencies: + jest: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + + jest-get-type@28.0.2: {} + + jest-get-type@29.6.3: {} + + jest-haste-map@28.1.3: + dependencies: + '@jest/types': 28.1.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 25.6.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + jest-worker: 28.1.3 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 25.6.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@28.1.3: + dependencies: + chalk: 4.1.2 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@28.1.3: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 28.1.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@28.0.2: {} + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@28.1.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jest/expect-utils': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/babel__traverse': 7.28.0 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 28.1.3 + graceful-fs: 4.2.11 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + jest-haste-map: 28.1.3 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + natural-compare: 1.4.0 + pretty-format: 28.1.3 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + jest-util@28.1.3: + dependencies: + '@jest/types': 28.1.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 25.6.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 25.6.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@28.1.3: + dependencies: + '@types/node': 25.6.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 25.6.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.17)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3)) + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + optional: true + + jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)): + dependencies: + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + optionalDependencies: + node-notifier: 10.0.1 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@1.21.7: {} + + jiti@2.6.1: {} + + jose@4.15.9: {} + + jpeg-js@0.4.4: {} + + jquery-deferred@0.3.1: {} + + jquery@3.7.1: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + optional: true + + js-cookie@3.0.5: + optional: true + + js-string-escape@1.0.1: {} + + js-tokens@10.0.0: {} + + js-tokens@3.0.2: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + + jsdoc-type-pratt-parser@4.8.0: {} + + jsdom@16.7.0: + dependencies: + abab: 2.0.6 + acorn: 8.16.0 + acorn-globals: 6.0.0 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 2.0.0 + decimal.js: 10.6.0 + domexception: 2.0.1 + escodegen: 2.1.0 + form-data: 3.0.4 + html-encoding-sniffer: 2.0.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 2.0.0 + webidl-conversions: 6.1.0 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + ws: 7.5.10 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@27.4.0(@noble/hashes@1.8.0): + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.20.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsdom@28.1.0(@noble/hashes@1.8.0): + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + cssstyle: 6.2.0 + data-urls: 7.0.0(@noble/hashes@1.8.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.24.6 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + + jsdom@29.0.1(@noble/hashes@1.8.0): + dependencies: + '@asamuzakjp/css-color': 5.0.1 + '@asamuzakjp/dom-selector': 7.0.4 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@1.8.0) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.7 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.24.6 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@0.3.0: {} + + jsesc@0.5.0: {} + + jsesc@1.3.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + + json-stringify-safe@5.0.1: {} + + json5@0.5.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonc-parser@3.3.1: {} + + jsonfile@2.4.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonify@0.0.1: {} + + jsonrepair@3.13.3: {} + + jsonwebtoken@8.5.1: + dependencies: + jws: 3.2.3 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 5.7.2 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + juice@9.1.0(encoding@0.1.13): + dependencies: + cheerio: 0.22.0 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + just-extend@4.2.1: {} + + just-extend@6.2.0: {} + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwk-to-pem@2.0.7: + dependencies: + asn1.js: 5.4.1 + elliptic: 6.6.1 + safe-buffer: 5.2.1 + + jwks-rsa@3.2.0: + dependencies: + '@types/express': 4.17.25 + '@types/jsonwebtoken': 9.0.10 + debug: 4.4.3(supports-color@5.5.0) + jose: 4.15.9 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + + jws@3.2.3: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + keymaster@https://codeload.github.com/madrobby/keymaster/tar.gz/f8f43ddafad663b505dc0908e72853bcf8daea49: {} + + keypair@1.0.4: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + knex-migrator@5.3.2: + dependencies: + '@tryghost/database-info': 0.3.22 + '@tryghost/errors': 1.3.13 + '@tryghost/logging': 2.5.5 + '@tryghost/promise': 0.3.8 + commander: 5.1.0 + compare-ver: 2.0.2 + debug: 4.4.1 + knex: 2.4.2(mysql2@3.14.1)(sqlite3@5.1.7) + lodash: 4.17.21 + moment: 2.24.0 + mysql2: 3.14.1 + nconf: 0.12.1 + resolve: 1.22.8 + optionalDependencies: + sqlite3: 5.1.7 + transitivePeerDependencies: + - better-sqlite3 + - bluebird + - mysql + - pg + - pg-native + - supports-color + - tedious + + knex@0.20.15(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7): + dependencies: + colorette: 1.1.0 + commander: 4.1.1 + debug: 4.1.1 + esm: 3.2.25 + getopts: 2.2.5 + inherits: 2.0.4 + interpret: 2.2.0 + liftoff: 3.1.0 + lodash: 4.17.23 + mkdirp: 0.5.6 + pg-connection-string: 2.1.0 + tarn: 2.0.0 + tildify: 2.0.0 + uuid: 7.0.3 + v8flags: 3.2.0 + optionalDependencies: + mysql2: 3.18.1(@types/node@22.19.17) + sqlite3: 5.1.7 + transitivePeerDependencies: + - supports-color + + knex@0.21.21(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7): + dependencies: + colorette: 1.2.1 + commander: 6.2.1 + debug: 4.3.1 + esm: 3.2.25 + getopts: 2.2.5 + interpret: 2.2.0 + liftoff: 3.1.0 + lodash: 4.17.23 + pg-connection-string: 2.4.0 + tarn: 3.0.2 + tildify: 2.0.0 + v8flags: 3.2.0 + optionalDependencies: + mysql2: 3.18.1(@types/node@22.19.17) + sqlite3: 5.1.7 + transitivePeerDependencies: + - supports-color + + knex@2.4.2(mysql2@3.14.1)(sqlite3@5.1.7): + dependencies: + colorette: 2.0.19 + commander: 9.5.0 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.23 + pg-connection-string: 2.5.0 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + mysql2: 3.14.1 + sqlite3: 5.1.7 + transitivePeerDependencies: + - supports-color + + knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7): + dependencies: + colorette: 2.0.19 + commander: 9.5.0 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.23 + pg-connection-string: 2.5.0 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + mysql2: 3.18.1(@types/node@22.19.17) + sqlite3: 5.1.7 + transitivePeerDependencies: + - supports-color + + knex@3.1.0(mysql2@3.18.1(@types/node@25.6.0)): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + mysql2: 3.18.1(@types/node@25.6.0) + transitivePeerDependencies: + - supports-color + + known-css-properties@0.29.0: {} + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + lazy-universal-dotenv@4.0.0: + dependencies: + app-root-dir: 1.0.2 + dotenv: 16.6.1 + dotenv-expand: 10.0.0 + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lead@4.0.0: {} + + leaky-bucket@2.2.0: + dependencies: + ee-argv: 0.1.4 + ee-class: 1.4.0 + ee-log: 3.0.9 + ee-types: 2.2.1 + + leek@0.0.24: + dependencies: + debug: 2.6.9 + lodash.assign: 3.2.0 + rsvp: 3.6.2 + transitivePeerDependencies: + - supports-color + + less-loader@11.1.4(less@4.6.4)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + less: 4.6.4 + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + + less-loader@11.1.4(less@4.6.4)(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + less: 4.6.4 + webpack: 5.105.4(@swc/core@1.15.21) + + less@4.6.4: + dependencies: + copy-anything: 3.0.5 + parse-node-version: 1.0.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.5.0 + source-map: 0.6.1 + + level-blobs@0.1.7: + dependencies: + level-peek: 1.0.6 + once: 1.4.0 + readable-stream: 1.1.14 + + level-filesystem@1.2.0: + dependencies: + concat-stream: 1.6.2 + errno: 0.1.8 + fwd-stream: 1.0.4 + level-blobs: 0.1.7 + level-peek: 1.0.6 + level-sublevel: 5.2.3 + octal: 1.0.0 + once: 1.4.0 + xtend: 2.2.0 + + level-fix-range@1.0.2: {} + + level-fix-range@2.0.0: + dependencies: + clone: 0.1.19 + + level-hooks@4.5.0: + dependencies: + string-range: 1.2.2 + + level-js@2.2.4: + dependencies: + abstract-leveldown: 0.12.4 + idb-wrapper: 1.7.2 + isbuffer: 0.0.0 + ltgt: 2.2.1 + typedarray-to-buffer: 1.0.4 + xtend: 2.1.2 + + level-peek@1.0.6: + dependencies: + level-fix-range: 1.0.2 + + level-sublevel@5.2.3: + dependencies: + level-fix-range: 2.0.0 + level-hooks: 4.5.0 + string-range: 1.2.2 + xtend: 2.0.6 + + levelup@0.18.6: + dependencies: + bl: 0.8.2 + deferred-leveldown: 0.2.0 + errno: 0.1.8 + prr: 0.0.0 + readable-stream: 1.0.34 + semver: 2.3.2 + xtend: 3.0.0 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lexical@0.13.1: {} + + libheif-js@1.19.8: {} + + lie@3.1.1: + dependencies: + immediate: 3.0.6 + + liftoff@3.1.0: + dependencies: + extend: 3.0.2 + findup-sync: 3.0.0 + fined: 1.2.0 + flagged-respawn: 1.0.1 + is-plain-object: 2.0.4 + object.map: 1.0.1 + rechoir: 0.6.2 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + lilconfig@3.1.3: {} + + limiter@1.1.5: {} + + line-column@1.0.2: + dependencies: + isarray: 1.0.0 + isobject: 2.1.0 + + lines-and-columns@1.2.4: {} + + lines-and-columns@2.0.3: {} + + linkify-it@2.2.0: + dependencies: + uc.micro: 1.0.6 + + linkify-it@3.0.3: + dependencies: + uc.micro: 1.0.6 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.3.2: {} + + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.1 + yaml: 2.8.3 + + liquid-fire@0.34.0(ember-source@3.24.0(@babel/core@7.29.0)): + dependencies: + '@embroider/macros': 1.16.13 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-stew: 3.0.0 + broccoli-string-replace: 0.1.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-cli-version-checker: 5.1.2 + ember-source: 3.24.0(@babel/core@7.29.0) + match-media: 0.2.0 + velocity-animate: 1.5.2 + transitivePeerDependencies: + - '@glint/template' + - supports-color + + liquid-wormhole@3.0.1(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0))(liquid-fire@0.34.0(ember-source@3.24.0(@babel/core@7.29.0))): + dependencies: + '@ember/jquery': 2.0.0 + '@ember/render-modifiers': 2.1.0(@babel/core@7.29.0)(ember-source@3.24.0(@babel/core@7.29.0)) + '@embroider/util': 1.13.5(ember-source@3.24.0(@babel/core@7.29.0)) + '@glimmer/component': 1.1.2(@babel/core@7.29.0) + '@glimmer/tracking': 1.1.2 + ember-cli-babel: 7.26.11 + ember-cli-htmlbars: 6.3.0 + ember-decorators: 6.1.1 + liquid-fire: 0.34.0(ember-source@3.24.0(@babel/core@7.29.0)) + perf-primitives: https://codeload.github.com/RobbieTheWagner/perf-primitives/tar.gz/a6a26f11497ca27be3763a88a5f20744e424756b + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - supports-color + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + livereload-js@3.4.1: {} + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + loader-runner@2.4.0: {} + + loader-runner@4.3.1: {} + + loader-utils@1.4.2: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + loader.js@4.7.0: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.2 + pkg-types: 1.3.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.0 + quansync: 0.2.11 + + localforage@1.10.0: + dependencies: + lie: 3.1.1 + + locate-character@2.0.5: {} + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.23: {} + + lodash-es@4.18.1: {} + + lodash._baseassign@3.2.0: + dependencies: + lodash._basecopy: 3.0.1 + lodash.keys: 3.1.2 + + lodash._basecopy@3.0.1: {} + + lodash._baseflatten@3.1.4: + dependencies: + lodash.isarguments: 3.1.0 + lodash.isarray: 3.0.4 + + lodash._baseiteratee@4.7.0: + dependencies: + lodash._stringtopath: 4.8.0 + + lodash._basetostring@4.12.0: {} + + lodash._baseuniq@4.6.0: + dependencies: + lodash._createset: 4.0.3 + lodash._root: 3.0.1 + + lodash._bindcallback@3.0.1: {} + + lodash._createassigner@3.1.1: + dependencies: + lodash._bindcallback: 3.0.1 + lodash._isiterateecall: 3.0.9 + lodash.restparam: 3.6.1 + + lodash._createset@4.0.3: {} + + lodash._getnative@3.9.1: {} + + lodash._isiterateecall@3.0.9: {} + + lodash._reinterpolate@3.0.0: {} + + lodash._root@3.0.1: {} + + lodash._stringtopath@4.8.0: + dependencies: + lodash._basetostring: 4.12.0 + + lodash.assign@3.2.0: + dependencies: + lodash._baseassign: 3.2.0 + lodash._createassigner: 3.1.1 + lodash.keys: 3.1.2 + + lodash.assignin@4.2.0: {} + + lodash.bind@4.2.1: {} + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.debounce@3.1.1: + dependencies: + lodash._getnative: 3.9.1 + + lodash.debounce@4.0.8: {} + + lodash.defaults@4.2.0: {} + + lodash.defaultsdeep@4.6.1: {} + + lodash.difference@4.5.0: {} + + lodash.filter@4.6.0: {} + + lodash.flatten@3.0.2: + dependencies: + lodash._baseflatten: 3.1.4 + lodash._isiterateecall: 3.0.9 + + lodash.flatten@4.4.0: {} + + lodash.foreach@4.5.0: {} + + lodash.get@4.4.2: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isarray@3.0.4: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.kebabcase@4.1.1: {} + + lodash.keys@3.1.2: + dependencies: + lodash._getnative: 3.9.1 + lodash.isarguments: 3.1.0 + lodash.isarray: 3.0.4 + + lodash.map@4.6.0: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.pick@4.4.0: {} + + lodash.reduce@4.6.0: {} + + lodash.reject@4.6.0: {} + + lodash.restparam@3.6.1: {} + + lodash.snakecase@4.1.1: {} + + lodash.some@4.6.0: {} + + lodash.template@4.5.0: + dependencies: + lodash._reinterpolate: 3.0.0 + lodash.templatesettings: 4.2.0 + + lodash.templatesettings@4.2.0: + dependencies: + lodash._reinterpolate: 3.0.0 + + lodash.truncate@4.4.2: {} + + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + + lodash.uniqby@4.5.0: + dependencies: + lodash._baseiteratee: 4.7.0 + lodash._baseuniq: 4.6.0 + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + lodash@4.17.23: {} + + lodash@4.18.1: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + logd-console-output@1.3.0: + dependencies: + app-root-path: 2.2.1 + chalk: 2.4.2 + ee-types: 2.2.1 + glob: 7.2.3 + + long-timeout@0.1.1: {} + + long@5.3.2: {} + + longest-streak@2.0.4: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + loupe@3.2.1: {} + + lower-case@1.1.4: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lowercase-keys@2.0.0: {} + + lowercase-keys@3.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@11.2.7: {} + + lru-cache@2.7.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + lru-memoizer@2.3.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + + lru.min@1.1.4: {} + + ltgt@2.2.1: {} + + lucide-react@0.577.0(react@18.3.1): + dependencies: + react: 18.3.1 + + luxon@3.7.2: {} + + lz-string@1.5.0: {} + + magic-string@0.24.1: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.25.9: + dependencies: + sourcemap-codec: 1.4.8 + + magic-string@0.27.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + mailgun.js@10.4.0: + dependencies: + axios: 1.13.6 + base-64: 1.0.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + + mailgun.js@8.2.2: + dependencies: + axios: 1.13.6 + base-64: 1.0.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-error@1.3.6: {} + + make-fetch-happen@15.0.5: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.0 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.2.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 1.0.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + make-iterator@1.0.1: + dependencies: + kind-of: 6.0.3 + + make-synchronized@0.8.0: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-cache@0.2.2: {} + + map-obj@1.0.1: {} + + map-obj@4.3.0: {} + + map-or-similar@1.5.0: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + markdown-escapes@1.0.4: {} + + markdown-it-footnote@4.0.0: {} + + markdown-it-image-lazy-loading@2.0.1: + dependencies: + image-size: 1.2.1 + + markdown-it-lazy-headers@0.1.3: {} + + markdown-it-mark@4.0.0: {} + + markdown-it-sub@2.0.0: {} + + markdown-it-sup@2.0.0: {} + + markdown-it-terminal@0.2.1: + dependencies: + ansi-styles: 3.2.1 + cardinal: 1.0.0 + cli-table: 0.3.11 + lodash.merge: 4.6.2 + markdown-it: 8.4.2 + + markdown-it@12.3.2: + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-it@8.4.2: + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 2.2.0 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + markdown-table@1.1.3: {} + + match-media@0.2.0: {} + + matcher-collection@1.1.2: + dependencies: + minimatch: 3.1.5 + + matcher-collection@2.0.1: + dependencies: + '@types/minimatch': 3.0.5 + minimatch: 3.1.5 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + mdast-util-compact@1.0.4: + dependencies: + unist-util-visit: 1.4.1 + + mdn-data@1.1.4: {} + + mdn-data@2.0.14: {} + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdn-data@2.0.4: {} + + mdn-data@2.27.1: {} + + mdurl@1.0.1: {} + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memoize-one@6.0.0: {} + + memoizerific@1.11.3: + dependencies: + map-or-similar: 1.5.0 + + memory-fs@0.4.1: + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + + memory-fs@0.5.0: + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + + memory-streams@0.1.3: + dependencies: + readable-stream: 1.0.34 + + memorystream@0.3.1: {} + + mensch@0.3.4: {} + + meow@10.1.5: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 7.0.2 + decamelize: 5.0.1 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 8.0.0 + redent: 4.0.0 + trim-newlines: 4.1.1 + type-fest: 1.4.0 + yargs-parser: 20.2.9 + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge-trees@1.0.1: + dependencies: + can-symlink: 1.0.0 + fs-tree-diff: 0.5.9 + heimdalljs: 0.2.6 + heimdalljs-logger: 0.1.10 + rimraf: 2.7.1 + symlink-or-copy: 1.3.1 + transitivePeerDependencies: + - supports-color + + merge-trees@2.0.0: + dependencies: + fs-updater: 1.0.4 + heimdalljs: 0.2.6 + transitivePeerDependencies: + - supports-color + + merge2@1.4.1: {} + + merge@1.2.1: {} + + metascraper-author@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-description@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-image@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-logo-favicon@5.42.0(@noble/hashes@1.8.0): + dependencies: + '@keyvhq/memoize': 2.0.3 + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + lodash: 4.17.23 + reachable-url: 1.7.2 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-logo@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + lodash: 4.17.23 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-publisher@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-title@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper-url@5.45.10(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + metascraper@5.45.15(@noble/hashes@1.8.0): + dependencies: + '@metascraper/helpers': 5.50.0(@noble/hashes@1.8.0) + cheerio: 1.0.0-rc.12 + lodash: 4.17.23 + whoops: 4.1.8 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - canvas + - supports-color + - utf-8-validate + + methods@1.1.2: {} + + microdiff@1.5.0: {} + + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + microsoft-capitalize@1.0.7: {} + + miller-rabin@4.0.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + + mime-db@1.25.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.13: + dependencies: + mime-db: 1.25.0 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mime@3.0.0: {} + + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@3.0.0: {} + + mimic-fn@3.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + mimic-response@4.0.0: {} + + min-indent@1.0.1: {} + + mingo@2.5.3: {} + + mini-css-extract-plugin@2.10.2(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + schema-utils: 4.3.3 + tapable: 2.3.2 + webpack: 5.105.4(@swc/core@1.15.21) + + mini-queue@0.0.14: + dependencies: + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@0.3.0: + dependencies: + lru-cache: 2.7.3 + sigmund: 1.0.1 + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.3 + + minimatch@8.0.7: + dependencies: + brace-expansion: 2.0.3 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.3 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.3 + + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + + minimist@0.0.8: {} + + minimist@0.2.4: {} + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.3 + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.2 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + + minipass@2.9.0: + dependencies: + safe-buffer: 5.2.1 + yallist: 3.1.1 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@4.2.8: {} + + minipass@5.0.0: + optional: true + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + optional: true + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + miragejs@0.1.48: + dependencies: + '@miragejs/pretender-node-polyfill': 0.1.2 + inflected: 2.1.0 + lodash: 4.17.23 + pretender: 3.4.7 + + mississippi@3.0.0: + dependencies: + concat-stream: 1.6.2 + duplexify: 3.7.1 + end-of-stream: 1.4.5 + flush-write-stream: 1.1.1 + from2: 2.3.0 + parallel-transform: 1.2.0 + pump: 3.0.4 + pumpify: 1.5.1 + stream-each: 1.2.3 + through2: 2.0.5 + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp-classic@0.5.3: {} + + mkdirp@0.3.0: {} + + mkdirp@0.5.1: + dependencies: + minimist: 0.0.8 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + mktemp@2.0.2: {} + + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mobiledoc-dom-renderer@0.7.0: + optional: true + + mobiledoc-dom-renderer@0.7.2: {} + + mobiledoc-text-renderer@0.4.0: + optional: true + + mocha-slow-test-reporter@0.1.2: + dependencies: + chalk: 1.1.3 + string-length: 1.0.1 + text-table: 0.2.0 + wordwrap: 1.0.0 + + mocha@11.7.5: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.5.0 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 9.0.9 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + mocha@2.5.3: + dependencies: + commander: 2.3.0 + debug: 2.2.0(supports-color@1.2.0) + diff: 1.4.0 + escape-string-regexp: 1.0.2 + glob: 3.2.11 + growl: 1.9.2 + jade: 0.26.3 + mkdirp: 0.5.1 + supports-color: 1.2.0 + to-iso-string: 0.0.2 + + mock-knex@https://codeload.github.com/TryGhost/mock-knex/tar.gz/68948e11b0ea4fe63456098dfdc169bea7f62009(knex@2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7)): + dependencies: + bluebird: 3.7.2 + knex: 2.4.2(mysql2@3.18.1(@types/node@22.19.17))(sqlite3@5.1.7) + lodash: 4.17.23 + semver: 5.7.2 + + module-details-from-path@1.0.4: {} + + moment-timezone@0.5.45: + dependencies: + moment: 2.24.0 + + moment@2.24.0: {} + + moo@0.5.3: {} + + morgan@1.10.1: + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.1.0 + transitivePeerDependencies: + - supports-color + + mout@1.2.4: {} + + move-concurrently@1.0.1: + dependencies: + aproba: 1.2.0 + copy-concurrently: 1.0.5 + fs-write-stream-atomic: 1.0.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + + mrmime@2.0.1: {} + + ms@0.7.1: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@25.6.0) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.2 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.5.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + multer@2.0.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mustache@4.2.0: {} + + mute-stream@0.0.7: {} + + mute-stream@0.0.8: {} + + mute-stream@2.0.0: {} + + mv@2.1.1: + dependencies: + mkdirp: 0.5.6 + ncp: 2.0.0 + rimraf: 2.4.5 + optional: true + + mysql2@3.14.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + mysql2@3.18.1(@types/node@22.19.17): + dependencies: + '@types/node': 22.19.17 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 + + mysql2@3.18.1(@types/node@25.6.0): + dependencies: + '@types/node': 25.6.0 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + najax@1.0.7: + dependencies: + jquery-deferred: 0.3.1 + lodash: 4.17.23 + qs: 6.15.0 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + nan@2.26.2: {} + + nanoclone@0.2.1: {} + + nanoid@3.3.11: {} + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + napi-build-utils@2.0.0: + optional: true + + natural-compare@1.4.0: {} + + nconf@0.12.1: + dependencies: + async: 3.2.6 + ini: 2.0.0 + secure-keys: 1.0.0 + yargs: 16.2.0 + + nconf@0.13.0: + dependencies: + async: 3.2.6 + ini: 2.0.0 + secure-keys: 1.0.0 + yargs: 16.2.0 + + ncp@2.0.0: + optional: true + + nearley@2.20.1: + dependencies: + commander: 2.20.3 + moo: 0.5.3 + railroad-diagrams: 1.0.0 + randexp: 0.4.6 + + needle@2.9.1: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.6.0 + transitivePeerDependencies: + - supports-color + + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.0 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + nice-try@1.0.5: {} + + nise@4.1.0: + dependencies: + '@sinonjs/commons': 1.8.6 + '@sinonjs/fake-timers': 6.0.1 + '@sinonjs/text-encoding': 0.7.3 + just-extend: 4.2.1 + path-to-regexp: 1.9.0 + + nise@6.1.4: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.1.1 + just-extend: 6.2.0 + path-to-regexp: 8.4.0 + + no-case@2.3.2: + dependencies: + lower-case: 1.1.4 + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + nock@13.5.6: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + json-stringify-safe: 5.0.1 + propagate: 2.0.1 + transitivePeerDependencies: + - supports-color + + node-abi@3.89.0: + dependencies: + semver: 7.7.4 + optional: true + + node-addon-api@7.1.1: + optional: true + + node-dir@0.1.17: + dependencies: + minimatch: 3.1.5 + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-forge@1.4.0: {} + + node-gyp@12.2.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + make-fetch-happen: 15.0.5 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + tar: 7.5.13 + tinyglobby: 0.2.15 + which: 6.0.1 + transitivePeerDependencies: + - supports-color + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.4 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + node-int64@0.4.0: {} + + node-jose@2.2.0: + dependencies: + base64url: 3.0.1 + buffer: 6.0.3 + es6-promise: 4.2.8 + lodash: 4.17.23 + long: 5.3.2 + node-forge: 1.4.0 + pako: 2.1.0 + process: 0.11.10 + uuid: 9.0.1 + + node-libs-browser@2.2.1: + dependencies: + assert: 1.5.1 + browserify-zlib: 0.2.0 + buffer: 4.9.2 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + crypto-browserify: 3.12.1 + domain-browser: 1.2.0 + events: 3.3.0 + https-browserify: 1.0.0 + os-browserify: 0.3.0 + path-browserify: 0.0.1 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 2.3.8 + stream-browserify: 2.0.2 + stream-http: 2.8.3 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.0 + url: 0.11.4 + util: 0.11.1 + vm-browserify: 1.1.2 + + node-loggly-bulk@3.0.1: + dependencies: + json-stringify-safe: 5.0.1 + moment: 2.24.0 + request: 2.88.2 + + node-machine-id@1.1.12: {} + + node-modules-path@1.0.2: {} + + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.7.4 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + + node-releases@2.0.36: {} + + nodemailer-direct-transport@3.3.2: + dependencies: + nodemailer-shared: 1.1.0 + smtp-connection: 2.12.0 + + nodemailer-fetch@1.6.0: {} + + nodemailer-mailgun-transport@2.1.5(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8): + dependencies: + consolidate: 0.15.1(babel-core@6.26.3)(handlebars@4.7.9)(lodash@4.17.23)(underscore@1.13.8) + form-data: 4.0.5 + mailgun.js: 8.2.2 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + + nodemailer-shared@1.1.0: + dependencies: + nodemailer-fetch: 1.6.0 + + nodemailer-stub-transport@1.1.0: {} + + nodemailer@6.10.1: {} + + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.4 + pstree.remy: 1.1.8 + semver: 7.7.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@3.0.6: + dependencies: + abbrev: 1.1.1 + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + optional: true + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.11 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.7.4 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@3.3.0: {} + + normalize-url@6.1.0: {} + + normalize-url@8.1.1: {} + + normalize.css@3.0.3: {} + + now-and-later@3.0.0: + dependencies: + once: 1.4.0 + + npm-git-info@1.0.3: {} + + npm-package-arg@8.1.5: + dependencies: + hosted-git-info: 4.1.0 + semver: 7.7.4 + validate-npm-package-name: 3.0.0 + + npm-run-all@4.1.5: + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.6 + memorystream: 0.3.1 + minimatch: 3.1.5 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.3 + string.prototype.padend: 3.1.6 + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@3.1.0: + dependencies: + path-key: 3.1.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + npmlog@4.1.2: + dependencies: + are-we-there-yet: 1.1.7 + console-control-strings: 1.1.0 + gauge: 2.7.4 + set-blocking: 2.0.0 + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + nth-check@1.0.2: + dependencies: + boolbase: 1.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + null-prototype-object@1.2.6: {} + + num2fraction@1.2.2: {} + + number-flow@0.5.8: + dependencies: + esm-env: 1.2.2 + + number-is-nan@1.0.1: {} + + numbered@1.1.0: {} + + nwsapi@2.2.23: {} + + nx@22.0.4(@swc/core@1.15.21): + dependencies: + '@napi-rs/wasm-runtime': 0.2.4 + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.2 + '@zkochan/js-yaml': 0.0.7 + axios: 1.13.6 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + enquirer: 2.3.6 + figures: 3.2.0 + flat: 5.0.2 + front-matter: 4.0.2 + ignore: 7.0.5 + jest-diff: 30.3.0 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + minimatch: 9.0.3 + node-machine-id: 1.1.12 + npm-run-path: 4.0.1 + open: 8.4.2 + ora: 5.3.0 + resolve.exports: 2.0.3 + semver: 7.7.4 + string-width: 4.2.3 + tar-stream: 2.2.0 + tmp: 0.2.5 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + yaml: 2.8.3 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 22.0.4 + '@nx/nx-darwin-x64': 22.0.4 + '@nx/nx-freebsd-x64': 22.0.4 + '@nx/nx-linux-arm-gnueabihf': 22.0.4 + '@nx/nx-linux-arm64-gnu': 22.0.4 + '@nx/nx-linux-arm64-musl': 22.0.4 + '@nx/nx-linux-x64-gnu': 22.0.4 + '@nx/nx-linux-x64-musl': 22.0.4 + '@nx/nx-win32-arm64-msvc': 22.0.4 + '@nx/nx-win32-x64-msvc': 22.0.4 + '@swc/core': 1.15.21 + transitivePeerDependencies: + - debug + + oauth-sign@0.9.0: {} + + object-assign@4.1.1: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-hash@1.3.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + + object-keys@0.2.0: + dependencies: + foreach: 2.0.6 + indexof: 0.0.1 + is: 0.2.7 + + object-keys@0.4.0: {} + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.defaults@1.1.0: + dependencies: + array-each: 1.0.1 + array-slice: 1.1.0 + for-own: 1.0.0 + isobject: 3.0.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.getownpropertydescriptors@2.1.9: + dependencies: + array.prototype.reduce: 1.0.8 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + gopd: 1.2.0 + safe-array-concat: 1.1.3 + + object.map@1.0.1: + dependencies: + for-own: 1.0.0 + make-iterator: 1.0.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + obug@2.1.1: {} + + octal@1.0.0: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optional-require@1.1.10: + dependencies: + require-at: 1.0.6 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + ora@5.3.0: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + orderedmap@2.1.1: {} + + os-browserify@0.3.0: {} + + os-homedir@1.0.2: {} + + os-tmpdir@1.0.2: {} + + osenv@0.1.5: + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + + otplib@12.0.1: + dependencies: + '@otplib/core': 12.0.1 + '@otplib/preset-default': 12.0.1 + '@otplib/preset-v11': 12.0.1 + + outvariant@1.4.3: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-cancelable@2.1.1: {} + + p-cancelable@3.0.0: {} + + p-cancelable@4.0.1: {} + + p-defer@3.0.0: {} + + p-finally@1.0.0: {} + + p-finally@2.0.1: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@2.1.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + p-map@7.0.4: {} + + p-reflect@2.1.0: {} + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + p-wait-for@3.1.0: + dependencies: + p-timeout: 3.2.0 + + p-wait-for@3.2.0: + dependencies: + p-timeout: 3.2.0 + + package-json-from-dist@1.0.1: {} + + pako@1.0.11: {} + + pako@2.1.0: {} + + papaparse@5.3.2: {} + + papaparse@5.5.3: {} + + parallel-transform@1.2.0: + dependencies: + cyclist: 1.0.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + + param-case@2.1.1: + dependencies: + no-case: 2.3.2 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-asn1@5.1.9: + dependencies: + asn1.js: 4.10.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.5 + safe-buffer: 5.2.1 + + parse-email-address@0.0.2: {} + + parse-entities@1.2.2: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@1.0.1: {} + + parse-ms@2.1.0: {} + + parse-ms@4.0.0: {} + + parse-node-version@1.0.1: {} + + parse-passwd@1.0.0: {} + + parse-prometheus-text-format@1.1.1: + dependencies: + shallow-equal: 1.2.1 + + parse-srcset@1.0.2: {} + + parse-static-imports@1.1.0: {} + + parse-uri@2.0.5: {} + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@6.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.0: + dependencies: + entities: 6.0.1 + + parseley@0.7.0: + dependencies: + moo: 0.5.3 + nearley: 2.20.1 + + parseurl@1.3.3: {} + + pascalcase@0.1.1: {} + + path-browserify@0.0.1: {} + + path-browserify@1.0.1: {} + + path-dirname@1.0.2: + optional: true + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-expression-matcher@1.2.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-match@1.2.4: + dependencies: + http-errors: 1.4.0 + path-to-regexp: 1.9.0 + + path-parse@1.0.7: {} + + path-posix@1.0.0: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.2.7 + minipass: 7.1.3 + + path-to-regexp@0.1.12: {} + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-to-regexp@6.3.0: {} + + path-to-regexp@8.4.0: {} + + path-type@3.0.0: + dependencies: + pify: 3.0.0 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + pathval@2.0.1: {} + + pbkdf2@3.1.5: + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + to-buffer: 1.2.2 + + peek-readable@4.1.0: {} + + pend@1.2.0: {} + + perf-primitives@https://codeload.github.com/RobbieTheWagner/perf-primitives/tar.gz/a6a26f11497ca27be3763a88a5f20744e424756b: + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + performance-now@2.1.0: {} + + pg-connection-string@2.1.0: {} + + pg-connection-string@2.4.0: {} + + pg-connection-string@2.5.0: {} + + pg-connection-string@2.6.2: {} + + pg-int8@1.0.1: {} + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + picocolors@0.2.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pidtree@0.3.1: {} + + pify@2.3.0: {} + + pify@3.0.0: {} + + pify@4.0.1: {} + + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + + pirates@4.0.7: {} + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-dir@5.0.0: + dependencies: + find-up: 5.0.0 + + pkg-entry-points@1.1.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + pkg-up@2.0.0: + dependencies: + find-up: 2.1.0 + + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + + playwright-core@1.59.1: {} + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + pngjs@6.0.0: {} + + polished@4.3.1: + dependencies: + '@babel/runtime': 7.29.2 + + popper.js@1.16.1: {} + + portfinder@1.0.38: + dependencies: + async: 3.2.6 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-attribute-case-insensitive@5.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-calc@10.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + + postcss-calc@7.0.5: + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-clamp@4.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-cli@11.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0): + dependencies: + chokidar: 3.6.0 + dependency-graph: 1.0.0 + fs-extra: 11.3.4 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-load-config: 5.1.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0) + postcss-reporter: 7.1.0(postcss@8.5.6) + pretty-hrtime: 1.0.3 + read-cache: 1.0.0 + slash: 5.1.0 + tinyglobby: 0.2.15 + yargs: 17.7.2 + transitivePeerDependencies: + - jiti + - tsx + + postcss-color-functional-notation@4.2.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-hex-alpha@8.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-mod-function@3.0.3: + dependencies: + '@csstools/convert-colors': 1.4.0 + postcss: 7.0.39 + postcss-values-parser: 2.0.1 + + postcss-color-rebeccapurple@7.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-colormin@4.0.3: + dependencies: + browserslist: 4.28.1 + color: 3.2.1 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-colormin@7.0.6(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-convert-values@4.0.1: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-convert-values@7.0.9(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-media@7.0.8: + dependencies: + postcss: 7.0.39 + + postcss-custom-media@8.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-properties@10.0.0: + dependencies: + postcss: 7.0.39 + postcss-values-parser: 4.0.0 + + postcss-custom-properties@12.1.11(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-selectors@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-dir-pseudo-class@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-discard-comments@4.0.2: + dependencies: + postcss: 7.0.39 + + postcss-discard-comments@7.0.6(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-discard-duplicates@4.0.2: + dependencies: + postcss: 7.0.39 + + postcss-discard-duplicates@7.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-discard-empty@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-overridden@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-discard-overridden@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-double-position-gradients@3.1.2(postcss@8.5.6): + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-env-function@4.0.6(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-focus-visible@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-focus-within@5.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-font-variant@5.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-gap-properties@3.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-image-set-function@4.0.7(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-import@12.0.1: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-import@16.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-initial@4.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-lab-function@4.2.1(postcss@8.5.6): + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-load-config@5.1.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + tsx: 4.21.0 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + tsx: 4.21.0 + yaml: 2.8.3 + + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + cosmiconfig: 8.3.6(typescript@5.9.3) + jiti: 1.21.7 + postcss: 8.5.6 + semver: 7.7.4 + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + transitivePeerDependencies: + - typescript + + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + cosmiconfig: 8.3.6(typescript@5.9.3) + jiti: 1.21.7 + postcss: 8.5.6 + semver: 7.7.4 + webpack: 5.105.4(@swc/core@1.15.21) + transitivePeerDependencies: + - typescript + + postcss-logical@5.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-media-minmax@5.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-merge-longhand@4.0.11: + dependencies: + css-color-names: 0.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + stylehacks: 4.0.3 + + postcss-merge-longhand@7.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + stylehacks: 7.0.8(postcss@8.5.6) + + postcss-merge-rules@4.0.3: + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + cssnano-util-same-parent: 4.0.1 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + vendors: 1.0.4 + + postcss-merge-rules@7.0.8(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-minify-font-values@4.0.2: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-minify-font-values@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + is-color-stop: 1.1.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-minify-gradients@7.0.1(postcss@8.5.6): + dependencies: + colord: 2.9.3 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-params@4.0.2: + dependencies: + alphanum-sort: 1.0.2 + browserslist: 4.28.1 + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + uniqs: 2.0.0 + + postcss-minify-params@7.0.6(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@4.0.2: + dependencies: + alphanum-sort: 1.0.2 + has: 1.0.4 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + + postcss-minify-selectors@7.0.6(postcss@8.5.6): + dependencies: + cssesc: 3.0.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-modules-values@4.0.0(postcss@8.5.6): + dependencies: + icss-utils: 5.1.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-nesting@10.2.0(postcss@8.5.6): + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-normalize-charset@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-normalize-charset@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-normalize-display-values@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-display-values@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-positions@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@4.0.2: + dependencies: + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-string@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@4.0.1: + dependencies: + browserslist: 4.28.1 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-unicode@7.0.6(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@4.0.1: + dependencies: + is-absolute-url: 2.1.0 + normalize-url: 3.3.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-url@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@4.0.2: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-whitespace@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-opacity-percentage@1.1.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-ordered-values@4.1.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-ordered-values@7.0.2(postcss@8.5.6): + dependencies: + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-overflow-shorthand@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-page-break@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-place@7.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-preset-env@7.8.3(postcss@8.5.6): + dependencies: + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.5.6) + '@csstools/postcss-color-function': 1.1.1(postcss@8.5.6) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.5.6) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.5.6) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.5.6) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.5.6) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.5.6) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.5.6) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.5.6) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.5.6) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.5.6) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.5.6) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.5.6) + autoprefixer: 10.4.21(postcss@8.5.6) + browserslist: 4.28.1 + css-blank-pseudo: 3.0.3(postcss@8.5.6) + css-has-pseudo: 3.0.4(postcss@8.5.6) + css-prefers-color-scheme: 6.0.3(postcss@8.5.6) + cssdb: 7.11.2 + postcss: 8.5.6 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.5.6) + postcss-clamp: 4.1.0(postcss@8.5.6) + postcss-color-functional-notation: 4.2.4(postcss@8.5.6) + postcss-color-hex-alpha: 8.0.4(postcss@8.5.6) + postcss-color-rebeccapurple: 7.1.1(postcss@8.5.6) + postcss-custom-media: 8.0.2(postcss@8.5.6) + postcss-custom-properties: 12.1.11(postcss@8.5.6) + postcss-custom-selectors: 6.0.3(postcss@8.5.6) + postcss-dir-pseudo-class: 6.0.5(postcss@8.5.6) + postcss-double-position-gradients: 3.1.2(postcss@8.5.6) + postcss-env-function: 4.0.6(postcss@8.5.6) + postcss-focus-visible: 6.0.4(postcss@8.5.6) + postcss-focus-within: 5.0.4(postcss@8.5.6) + postcss-font-variant: 5.0.0(postcss@8.5.6) + postcss-gap-properties: 3.0.5(postcss@8.5.6) + postcss-image-set-function: 4.0.7(postcss@8.5.6) + postcss-initial: 4.0.1(postcss@8.5.6) + postcss-lab-function: 4.2.1(postcss@8.5.6) + postcss-logical: 5.0.4(postcss@8.5.6) + postcss-media-minmax: 5.0.0(postcss@8.5.6) + postcss-nesting: 10.2.0(postcss@8.5.6) + postcss-opacity-percentage: 1.1.3(postcss@8.5.6) + postcss-overflow-shorthand: 3.0.4(postcss@8.5.6) + postcss-page-break: 3.0.4(postcss@8.5.6) + postcss-place: 7.0.5(postcss@8.5.6) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.5.6) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6) + postcss-selector-not: 6.0.1(postcss@8.5.6) + postcss-value-parser: 4.2.0 + + postcss-pseudo-class-any-link@7.1.6(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-reduce-initial@4.0.3: + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + has: 1.0.4 + postcss: 7.0.39 + + postcss-reduce-initial@7.0.6(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + caniuse-api: 3.0.0 + postcss: 8.5.6 + + postcss-reduce-transforms@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-reduce-transforms@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-reporter@7.1.0(postcss@8.5.6): + dependencies: + picocolors: 1.1.1 + postcss: 8.5.6 + thenby: 1.3.4 + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-not@6.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@3.1.2: + dependencies: + dot-prop: 5.3.0 + indexes-of: 1.0.1 + uniq: 1.0.1 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-svgo@4.0.3: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + svgo: 1.3.2 + + postcss-svgo@7.1.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + svgo: 4.0.1 + + postcss-unique-selectors@4.0.1: + dependencies: + alphanum-sort: 1.0.2 + postcss: 7.0.39 + uniqs: 2.0.0 + + postcss-unique-selectors@7.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + postcss-value-parser@3.3.1: {} + + postcss-value-parser@4.2.0: {} + + postcss-values-parser@2.0.1: + dependencies: + flatten: 1.0.3 + indexes-of: 1.0.1 + uniq: 1.0.1 + + postcss-values-parser@4.0.0: + dependencies: + color-name: 1.1.4 + is-url-superb: 4.0.0 + postcss: 7.0.39 + + postcss@7.0.39: + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.89.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + optional: true + + prelude-ls@1.2.1: {} + + pretender@3.4.7: + dependencies: + fake-xml-http-request: 2.1.2 + route-recognizer: 0.3.4 + + prettier@2.8.8: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@28.1.3: + dependencies: + '@jest/schemas': 28.1.3 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + pretty-hrtime@1.0.3: {} + + pretty-ms@3.2.0: + dependencies: + parse-ms: 1.0.1 + + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prettyjson@1.2.5: + dependencies: + colors: 1.4.0 + minimist: 1.2.8 + + printf@0.6.1: {} + + prismjs@1.30.0: {} + + private@0.1.8: {} + + probability-distributions@0.9.1: + dependencies: + crypto: 0.0.3 + + probe-image-size@7.2.3: + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + stream-parser: 0.3.1 + transitivePeerDependencies: + - supports-color + + proc-log@5.0.0: {} + + proc-log@6.1.0: {} + + process-es6@0.11.6: {} + + process-nextick-args@2.0.1: {} + + process-relative-require@1.0.0: + dependencies: + node-modules-path: 1.0.2 + + process@0.11.10: {} + + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 + + promise-inflight@1.0.1(bluebird@3.7.2): + optionalDependencies: + bluebird: 3.7.2 + + promise-map-series@0.2.3: + dependencies: + rsvp: 3.6.2 + + promise-map-series@0.3.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + promise.hash.helper@1.0.8: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + propagate@2.0.1: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + property-expr@2.0.6: {} + + prosemirror-changeset@2.4.0: + dependencies: + prosemirror-transform: 1.11.0 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.4 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.4: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.1 + prosemirror-model: 1.25.4 + + prosemirror-menu@1.3.0: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.5.0 + prosemirror-state: 1.4.4 + + prosemirror-model@1.25.4: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + prosemirror-view: 1.41.7 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.7): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.7 + + prosemirror-transform@1.11.0: + dependencies: + prosemirror-model: 1.25.4 + + prosemirror-view@1.41.7: + dependencies: + prosemirror-model: 1.25.4 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.11.0 + + proto-list@1.2.4: + optional: true + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.6.0 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@1.1.0: {} + + proxy-from-env@2.1.0: {} + + prr@0.0.0: {} + + prr@1.0.1: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pstree.remy@1.1.8: {} + + public-encrypt@4.0.3: + dependencies: + bn.js: 4.12.3 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + parse-asn1: 5.1.9 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + pump@2.0.1: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pumpify@1.5.1: + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + + punycode.js@2.3.1: {} + + punycode2@1.0.1: {} + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + q@1.5.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + qs@6.5.5: {} + + quansync@0.2.11: {} + + querystring-es3@0.2.1: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + queue@6.0.2: + dependencies: + inherits: 2.0.4 + + quick-lru@5.1.1: {} + + quick-temp@0.1.9: + dependencies: + mktemp: 2.0.2 + rimraf: 5.0.10 + underscore.string: 3.3.6 + + raf-pool@0.1.4: {} + + railroad-diagrams@1.0.0: {} + + ramda@0.27.2: {} + + ramda@0.29.0: {} + + randexp@0.4.6: + dependencies: + discontinuous-range: 1.0.0 + ret: 0.1.15 + + random-bytes@1.0.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + randomfill@1.0.4: + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@1.1.7: + dependencies: + bytes: 1.0.0 + string_decoder: 0.10.31 + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + re2@1.23.3: + dependencies: + install-artifact-from-github: 1.4.0 + nan: 2.26.2 + node-gyp: 12.2.0 + transitivePeerDependencies: + - supports-color + + reachable-url@1.7.2: + dependencies: + got: 11.8.6 + p-reflect: 2.1.0 + + react-colorful@5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-docgen-typescript@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + react-docgen@7.1.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.11 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + + react-docgen@8.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.11 + strip-indent: 4.1.1 + transitivePeerDependencies: + - supports-color + + react-dom@17.0.2(react@17.0.2): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 17.0.2 + scheduler: 0.20.2 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@14.2.3(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 0.6.0 + prop-types: 15.8.1 + react: 18.3.1 + + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + + react-hook-form@7.72.1(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.2.3 + goober: 2.1.18(csstype@3.2.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-remove-scroll@2.5.5(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + react-remove-scroll@2.7.2(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + react-router@7.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.1.1 + react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-select@5.10.2(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@floating-ui/dom': 1.7.6 + '@types/react-transition-group': 4.4.12(@types/react@18.3.28) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + use-isomorphic-layout-effect: 1.2.1(@types/react@18.3.28)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-string-replace@1.1.1: {} + + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-svg-map@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-world-flags@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + svg-country-flags: 1.2.10 + svgo: 3.3.3 + world-countries: 5.1.0 + + react@17.0.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg-up@8.0.0: + dependencies: + find-up: 5.0.0 + read-pkg: 6.0.0 + type-fest: 1.4.0 + + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + read-pkg@6.0.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 1.4.0 + + readable-stream@1.0.34: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@1.1.14: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + readdirp@2.2.1: + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10 + readable-stream: 2.3.8 + transitivePeerDependencies: + - supports-color + optional: true + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + recast@0.18.10: + dependencies: + ast-types: 0.13.3 + esprima: 4.0.1 + private: 0.1.8 + source-map: 0.6.1 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + rechoir@0.6.2: + dependencies: + resolve: 1.22.11 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.11 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redent@4.0.0: + dependencies: + indent-string: 5.0.0 + strip-indent: 4.1.1 + + redeyed@1.0.1: + dependencies: + esprima: 3.0.0 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + reframe.js@4.0.2: {} + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.10.5: {} + + regenerator-runtime@0.11.1: {} + + regenerator-runtime@0.13.11: {} + + regenerator-runtime@0.9.6: {} + + regenerator-transform@0.10.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + private: 0.1.8 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regex-parser@2.3.1: {} + + regexp-tree@0.1.27: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@2.0.0: + dependencies: + regenerate: 1.4.2 + regjsgen: 0.2.0 + regjsparser: 0.1.5 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.2.0: {} + + regjsgen@0.8.0: {} + + regjsparser@0.1.5: + dependencies: + jsesc: 0.5.0 + + regjsparser@0.13.0: + dependencies: + jsesc: 3.1.0 + + relateurl@0.2.7: {} + + remark-footnotes@1.0.0: {} + + remark-parse@7.0.2: + dependencies: + collapse-white-space: 1.0.6 + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + is-whitespace-character: 1.0.4 + is-word-character: 1.0.4 + markdown-escapes: 1.0.4 + parse-entities: 1.2.2 + repeat-string: 1.6.1 + state-toggle: 1.0.3 + trim: 0.0.1 + trim-trailing-lines: 1.1.4 + unherit: 1.1.3 + unist-util-remove-position: 1.1.4 + vfile-location: 2.0.6 + xtend: 4.0.2 + + remark-stringify@7.0.4: + dependencies: + ccount: 1.1.0 + is-alphanumeric: 1.0.0 + is-decimal: 1.0.4 + is-whitespace-character: 1.0.4 + longest-streak: 2.0.4 + markdown-escapes: 1.0.4 + markdown-table: 1.1.3 + mdast-util-compact: 1.0.4 + parse-entities: 1.2.2 + repeat-string: 1.6.1 + state-toggle: 1.0.3 + stringify-entities: 2.0.0 + unherit: 1.1.3 + xtend: 4.0.2 + + remark@11.0.2: + dependencies: + remark-parse: 7.0.2 + remark-stringify: 7.0.4 + unified: 8.4.2 + + remove-trailing-separator@1.1.0: {} + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + repeating@2.0.1: + dependencies: + is-finite: 1.1.0 + + replace-ext@2.0.0: {} + + reqresnext@1.7.0: + dependencies: + express: 4.21.2 + lodash: 4.17.23 + setprototypeof: 1.2.0 + transitivePeerDependencies: + - supports-color + + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.5 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + + require-at@1.0.6: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + require-relative@0.8.7: {} + + requireindex@1.1.0: {} + + requireindex@1.2.0: {} + + requires-port@1.0.0: {} + + reselect@3.0.1: {} + + reselect@4.1.8: {} + + resolve-alpn@1.2.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@3.0.0: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-options@2.0.0: + dependencies: + value-or-function: 4.0.0 + + resolve-package-path@1.2.7: + dependencies: + path-root: 0.1.1 + resolve: 1.22.11 + + resolve-package-path@2.0.0: + dependencies: + path-root: 0.1.1 + resolve: 1.22.11 + + resolve-package-path@3.1.0: + dependencies: + path-root: 0.1.1 + resolve: 1.22.11 + + resolve-package-path@4.0.3: + dependencies: + path-root: 0.1.1 + + resolve-path@1.4.0: + dependencies: + http-errors: 1.6.3 + path-is-absolute: 1.0.1 + + resolve-pkg-maps@1.0.0: {} + + resolve-url-loader@5.0.0: + dependencies: + adjust-sourcemap-loader: 4.0.0 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 + postcss: 8.5.6 + source-map: 0.6.1 + + resolve-url@0.2.1: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@1.22.8: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + responselike@4.0.2: + dependencies: + lowercase-keys: 3.0.0 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + ret@0.1.15: {} + + retry@0.12.0: {} + + rettime@0.10.1: {} + + reusify@1.1.0: {} + + rewire@9.0.1(jiti@2.6.1): + dependencies: + eslint: 9.37.0(jiti@2.6.1) + pirates: 4.0.7 + transitivePeerDependencies: + - jiti + - supports-color + + rfdc@1.4.1: {} + + rgb-regex@1.0.1: {} + + rgba-regex@1.0.0: {} + + rimraf@2.4.5: + dependencies: + glob: 6.0.4 + optional: true + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + + rollup-plugin-node-builtins@2.1.2: + dependencies: + browserify-fs: 1.0.0 + buffer-es6: 4.9.3 + crypto-browserify: 3.12.1 + process-es6: 0.11.6 + + rollup-pluginutils@2.8.2: + dependencies: + estree-walker: 0.6.1 + + rollup@0.57.1: + dependencies: + '@types/acorn': 4.0.6 + acorn: 5.7.4 + acorn-dynamic-import: 3.0.0 + date-time: 2.1.0 + is-reference: 1.2.1 + locate-character: 2.0.5 + pretty-ms: 3.2.0 + require-relative: 0.8.7 + rollup-pluginutils: 2.8.2 + signal-exit: 3.0.7 + sourcemap-codec: 1.4.8 + + rollup@1.32.1: + dependencies: + '@types/estree': 1.0.8 + '@types/node': 25.6.0 + acorn: 7.4.1 + + rollup@4.60.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 + fsevents: 2.3.3 + + rope-sequence@1.3.4: {} + + route-recognizer@0.3.4: {} + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.0 + transitivePeerDependencies: + - supports-color + + rss@1.2.2: + dependencies: + mime-types: 2.1.13 + xml: 1.0.1 + + rsvp@3.2.1: {} + + rsvp@3.6.2: {} + + rsvp@4.8.5: {} + + run-applescript@7.1.0: {} + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + run-queue@1.0.3: + dependencies: + aproba: 1.2.0 + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-json-parse@1.0.1: {} + + safe-json-stringify@1.2.0: + optional: true + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + + safe-stable-stringify@2.5.0: {} + + safe-timers@1.1.0: {} + + safer-buffer@2.1.2: {} + + sane@4.1.0: + dependencies: + '@cnakazawa/watch': 1.0.4 + anymatch: 2.0.0 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 1.0.0 + fb-watchman: 2.0.2 + micromatch: 3.1.10 + minimist: 1.2.8 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + sanitize-html@2.17.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + + sass-loader@13.3.3(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + neo-async: 2.6.2 + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + + sass-loader@13.3.3(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + neo-async: 2.6.2 + webpack: 5.105.4(@swc/core@1.15.21) + + sax@1.2.4: {} + + sax@1.6.0: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.20.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@1.0.0: + dependencies: + ajv: 6.14.0 + ajv-errors: 1.0.1(ajv@6.14.0) + ajv-keywords: 3.5.2(ajv@6.14.0) + + schema-utils@2.7.1: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) + + section-tests@1.3.1: + dependencies: + '@distributed-systems/callsite': 1.1.1 + chalk: 1.1.3 + ee-log: 1.1.0 + ee-types: 2.2.1 + glob: 7.2.3 + + secure-json-parse@2.7.0: {} + + secure-keys@1.0.0: {} + + selderee@0.6.0: + dependencies: + parseley: 0.7.0 + + semver@2.3.2: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.3: {} + + semver@7.7.4: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + sentry-testkit@5.0.10: + dependencies: + body-parser: 1.20.3 + express: 4.21.2 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serialize-javascript@4.0.0: + dependencies: + randombytes: 2.1.0 + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-cookie-parser@2.7.2: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + setimmediate@1.0.5: {} + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shallow-equal@1.2.1: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + 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 + optional: true + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + shellwords@0.1.1: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + sigmund@1.0.1: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + silent-error@1.1.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + simple-concat@1.0.1: + optional: true + + simple-dom@1.4.0: + dependencies: + '@simple-dom/document': 1.4.0 + '@simple-dom/interface': 1.4.0 + '@simple-dom/parser': 1.4.0 + '@simple-dom/serializer': 1.4.0 + '@simple-dom/void-map': 1.4.0 + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + simple-html-tokenizer@0.5.11: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + sinon-chai@4.0.1(chai@4.5.0)(sinon@21.1.1): + dependencies: + chai: 4.5.0 + sinon: 21.1.1 + + sinon@18.0.1: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 11.2.2 + '@sinonjs/samsam': 8.0.3 + diff: 5.2.2 + nise: 6.1.4 + supports-color: 7.2.0 + + sinon@21.0.1: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.1.1 + '@sinonjs/samsam': 8.0.3 + diff: 8.0.4 + supports-color: 7.2.0 + + sinon@21.1.1: + dependencies: + '@sinonjs/commons': 3.0.1 + '@sinonjs/fake-timers': 15.3.1 + '@sinonjs/samsam': 10.0.1 + diff: 8.0.4 + npm-run-all: 4.1.5 + + sinon@9.2.4: + dependencies: + '@sinonjs/commons': 1.8.6 + '@sinonjs/fake-timers': 6.0.1 + '@sinonjs/samsam': 5.3.1 + diff: 4.0.4 + nise: 4.1.0 + supports-color: 7.2.0 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + slash@1.0.0: {} + + slash@3.0.0: {} + + slash@4.0.0: {} + + slash@5.1.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slick@1.12.2: {} + + smart-buffer@4.2.0: {} + + smartquotes@2.3.2: {} + + smtp-connection@2.12.0: + dependencies: + httpntlm: 1.6.1 + nodemailer-shared: 1.1.0 + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + socket.io-adapter@2.5.6: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.4.3(supports-color@5.5.0) + engine.io: 6.6.6 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + optional: true + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + sort-keys@5.1.0: + dependencies: + is-plain-obj: 4.1.0 + + sort-object-keys@1.1.3: {} + + sort-package-json@1.57.0: + dependencies: + detect-indent: 6.1.0 + detect-newline: 3.1.0 + git-hooks-list: 1.0.3 + globby: 10.0.0 + is-plain-obj: 2.1.0 + sort-object-keys: 1.1.3 + + source-list-map@2.0.1: {} + + source-map-js@1.2.1: {} + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-support@0.4.18: + dependencies: + source-map: 0.5.7 + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-url@0.3.0: {} + + source-map-url@0.4.1: {} + + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + + source-map@0.4.4: + dependencies: + amdefine: 1.0.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + sourcemap-codec@1.4.8: {} + + sourcemap-validator@1.1.1: + dependencies: + jsesc: 0.3.0 + lodash.foreach: 4.5.0 + lodash.template: 4.5.0 + source-map: 0.1.43 + + spawn-args@0.2.0: {} + + spawn-command@0.0.2: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + split-ca@1.0.1: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: {} + + sql-escaper@1.3.3: {} + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + sqlstring@2.3.3: {} + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.26.2 + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + + ssri@6.0.2: + dependencies: + figgy-pudding: 3.5.2 + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + stable@0.1.8: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackback@0.0.2: {} + + stagehand@1.0.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + standard-as-callback@2.1.0: {} + + state-toggle@1.0.3: {} + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + std-env@4.0.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stoppable@1.1.0: {} + + store2@2.14.4: {} + + storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) + '@vitest/expect': 3.2.4 + '@vitest/spy': 3.2.4 + esbuild: 0.25.12 + open: 10.2.0 + recast: 0.23.11 + semver: 7.7.3 + use-sync-external-store: 1.6.0(react@18.3.1) + ws: 8.20.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - react + - react-dom + - utf-8-validate + + storybook@8.6.15(prettier@2.8.8): + dependencies: + '@storybook/core': 8.6.15(prettier@2.8.8)(storybook@8.6.15(prettier@2.8.8)) + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + stream-browserify@2.0.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + stream-composer@1.0.2: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + stream-each@1.2.3: + dependencies: + end-of-stream: 1.4.5 + stream-shift: 1.0.3 + + stream-http@2.8.3: + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.8 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + + stream-parser@0.3.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + stream-shift@1.0.3: {} + + streamsearch@1.1.0: {} + + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + strict-event-emitter@0.5.1: {} + + string-argv@0.3.2: {} + + string-length@1.0.1: + dependencies: + strip-ansi: 3.0.1 + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-range@1.2.2: {} + + string-template@0.2.1: {} + + string-width@1.0.2: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.padend@3.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@0.10.31: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@2.0.0: + dependencies: + character-entities-html4: 1.1.4 + character-entities-legacy: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.1.1: {} + + strip-json-comments@2.0.1: + optional: true + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stripe@8.222.0: + dependencies: + '@types/node': 25.6.0 + qs: 6.15.0 + + strnum@2.2.2: {} + + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + + style-loader@2.0.0(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.105.4(@swc/core@1.15.21) + + style-loader@3.3.4(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + + style-loader@3.3.4(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + webpack: 5.105.4(@swc/core@1.15.21) + + style-mod@4.1.3: {} + + style-search@0.1.0: {} + + styled_string@0.0.1: {} + + stylehacks@4.0.3: + dependencies: + browserslist: 4.28.1 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + + stylehacks@7.0.8(postcss@8.5.6): + dependencies: + browserslist: 4.28.1 + postcss: 8.5.6 + postcss-selector-parser: 7.1.1 + + stylelint@15.11.0(typescript@5.9.3): + dependencies: + '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) + '@csstools/css-tokenizer': 2.4.1 + '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) + '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + css-functions-list: 3.3.3 + css-tree: 2.3.1 + debug: 4.4.3(supports-color@5.5.0) + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 7.0.2 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.3.2 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.29.0 + mathml-tag-names: 2.1.3 + meow: 10.1.5 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + stylis@4.2.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + sum-up@1.0.3: + dependencies: + chalk: 1.1.3 + + superagent-throttle@1.0.1: {} + + superagent@5.3.1: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 3.0.4 + formidable: 1.2.6 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.0 + readable-stream: 3.6.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + superagent@8.1.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 2.1.5 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + supertest@6.3.4: + dependencies: + methods: 1.1.2 + superagent: 8.1.2 + transitivePeerDependencies: + - supports-color + + supports-color@1.2.0: {} + + supports-color@2.0.0: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-country-flags@1.2.10: {} + + svg-parser@2.0.4: {} + + svg-tags@1.0.0: {} + + svgo@1.3.0: + dependencies: + chalk: 2.4.2 + coa: 2.0.2 + css-select: 2.1.0 + css-select-base-adapter: 0.1.1 + css-tree: 1.0.0-alpha.33 + csso: 3.5.1 + js-yaml: 3.14.2 + mkdirp: 0.5.6 + object.values: 1.2.1 + sax: 1.2.4 + stable: 0.1.8 + unquote: 1.1.1 + util.promisify: 1.0.1 + + svgo@1.3.2: + dependencies: + chalk: 2.4.2 + coa: 2.0.2 + css-select: 2.1.0 + css-select-base-adapter: 0.1.1 + css-tree: 1.0.0-alpha.37 + csso: 4.2.0 + js-yaml: 3.14.2 + mkdirp: 0.5.6 + object.values: 1.2.1 + sax: 1.2.4 + stable: 0.1.8 + unquote: 1.1.1 + util.promisify: 1.0.1 + + svgo@3.3.3: + dependencies: + commander: 7.2.0 + css-select: 5.2.2 + css-tree: 2.3.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + svgo@4.0.1: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.0 + + swr@2.4.1(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + + symbol-tree@3.2.4: {} + + symlink-or-copy@1.3.1: {} + + sync-disk-cache@1.3.4: + dependencies: + debug: 2.6.9 + heimdalljs: 0.2.6 + mkdirp: 0.5.6 + rimraf: 2.7.1 + username-sync: 1.0.3 + transitivePeerDependencies: + - supports-color + + synchronous-promise@2.0.17: {} + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + sywac@1.3.0: {} + + tabbable@5.3.3: {} + + table@6.9.0: + dependencies: + ajv: 8.18.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tagged-tag@1.0.0: {} + + tailwind-api-utils@1.0.3(tailwindcss@4.2.1): + dependencies: + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + local-pkg: 1.1.2 + tailwindcss: 4.2.1 + + tailwind-api-utils@1.0.3(tailwindcss@4.2.2): + dependencies: + enhanced-resolve: 5.20.1 + jiti: 2.6.1 + local-pkg: 1.1.2 + tailwindcss: 4.2.2 + + tailwind-merge@3.5.0: {} + + tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.3) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + tailwindcss@4.2.1: {} + + tailwindcss@4.2.2: {} + + tap-parser@7.0.0: + dependencies: + events-to-array: 1.1.2 + js-yaml: 3.14.2 + minipass: 2.9.0 + + tapable@1.1.3: {} + + tapable@2.3.2: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.1.8: + dependencies: + b4a: 1.8.0 + bare-fs: 4.5.6 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + optional: true + + tar@7.5.13: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + tarn@2.0.0: {} + + tarn@3.0.2: {} + + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + telejson@7.2.0: + dependencies: + memoizerific: 1.11.3 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + + terser-webpack-plugin@1.4.6(webpack@4.47.0): + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + is-wsl: 1.1.0 + schema-utils: 1.0.0 + serialize-javascript: 4.0.0 + source-map: 0.6.1 + terser: 4.8.1 + webpack: 4.47.0 + webpack-sources: 1.4.3 + worker-farm: 1.7.0 + + terser-webpack-plugin@5.4.0(@swc/core@1.15.21)(esbuild@0.25.12)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.25.12) + optionalDependencies: + '@swc/core': 1.15.21 + esbuild: 0.25.12 + + terser-webpack-plugin@5.4.0(@swc/core@1.15.21)(esbuild@0.27.4)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.105.4(@swc/core@1.15.21)(esbuild@0.27.4) + optionalDependencies: + '@swc/core': 1.15.21 + esbuild: 0.27.4 + optional: true + + terser-webpack-plugin@5.4.0(@swc/core@1.15.21)(webpack@5.105.4(@swc/core@1.15.21)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.105.4(@swc/core@1.15.21) + optionalDependencies: + '@swc/core': 1.15.21 + + terser@4.8.1: + dependencies: + acorn: 8.16.0 + commander: 2.20.3 + source-map: 0.6.1 + source-map-support: 0.5.21 + + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.5 + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 + + testem@3.19.1(@babel/core@7.29.0)(handlebars@4.7.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8): + dependencies: + '@xmldom/xmldom': 0.8.11 + backbone: 1.6.1 + charm: 1.0.2 + commander: 2.20.3 + compression: 1.8.1 + consolidate: 1.0.4(@babel/core@7.29.0)(handlebars@4.7.9)(lodash@4.17.23)(mustache@4.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(underscore@1.13.8) + execa: 9.6.1 + express: 4.21.2 + fireworm: 0.7.2 + glob: 13.0.6 + http-proxy: 1.18.1 + js-yaml: 3.14.2 + lodash: 4.17.23 + mkdirp: 3.0.1 + mustache: 4.2.0 + node-notifier: 10.0.1 + printf: 0.6.1 + proc-log: 5.0.0 + rimraf: 6.1.3 + socket.io: 4.8.3 + spawn-args: 0.2.0 + styled_string: 0.0.1 + tap-parser: 7.0.0 + tmp: 0.2.5 + transitivePeerDependencies: + - '@babel/core' + - arc-templates + - atpl + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jazz + - jqtpl + - just + - liquid-node + - liquor + - mote + - nunjucks + - plates + - pug + - qejs + - ractive + - react + - react-dom + - slm + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - whiskers + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.0 + transitivePeerDependencies: + - react-native-b4a + + text-table@0.2.0: {} + + textextensions@2.6.0: {} + + thenby@1.3.4: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thirty-two@1.0.2: {} + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through2@3.0.2: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + + through@2.3.8: {} + + tildify@2.0.0: {} + + time-zone@1.0.0: {} + + timers-browserify@2.0.12: + dependencies: + setimmediate: 1.0.5 + + timsort@0.3.0: {} + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + tiny-invariant@1.3.3: {} + + tiny-lr@2.0.0: + dependencies: + body: 5.1.0 + debug: 3.2.7 + faye-websocket: 0.11.4 + livereload-js: 3.4.1 + object-assign: 4.1.1 + qs: 6.15.0 + transitivePeerDependencies: + - supports-color + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@0.8.4: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyrainbow@2.0.0: {} + + tinyrainbow@3.1.0: {} + + tinyspy@2.2.1: {} + + tinyspy@3.0.2: {} + + tinyspy@4.0.4: {} + + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + tlds@1.261.0: {} + + tldts-core@7.0.27: {} + + tldts@7.0.27: + dependencies: + tldts-core: 7.0.27 + + tmp@0.0.28: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.1.0: + dependencies: + rimraf: 2.7.1 + + tmp@0.2.5: {} + + tmpl@1.0.5: {} + + to-arraybuffer@1.0.1: {} + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + to-fast-properties@1.0.3: {} + + to-iso-string@0.0.2: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + to-through@3.0.0: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + toidentifier@1.0.1: {} + + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + toml@3.0.0: {} + + tooltip.js@1.3.3: + dependencies: + popper.js: 1.16.1 + + toposort@2.0.2: {} + + totalist@3.0.1: {} + + touch@3.1.1: {} + + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.27 + + tr46@0.0.3: {} + + tr46@2.1.0: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tracked-built-ins@3.4.0(@babel/core@7.29.0): + dependencies: + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 2.3.1(@babel/core@7.29.0) + ember-tracked-storage-polyfill: 1.0.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + tree-kill@1.2.2: {} + + tree-sync@1.4.0: + dependencies: + debug: 2.6.9 + fs-tree-diff: 0.5.9 + mkdirp: 0.5.6 + quick-temp: 0.1.9 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + tree-sync@2.1.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + fs-tree-diff: 2.0.1 + mkdirp: 0.5.6 + quick-temp: 0.1.9 + walk-sync: 0.3.4 + transitivePeerDependencies: + - supports-color + + trim-newlines@4.1.1: {} + + trim-right@1.0.1: {} + + trim-trailing-lines@1.1.4: {} + + trim@0.0.1: {} + + trough@1.0.5: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): + dependencies: + picomatch: 4.0.4 + typescript: 5.9.3 + + ts-dedent@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 29.7.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + jest-util: 29.7.0 + + ts-node@10.9.2(@swc/core@1.15.21)(@types/node@22.19.17)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.17 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.21 + optional: true + + ts-node@10.9.2(@swc/core@1.15.21)(@types/node@25.6.0)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 25.6.0 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.21 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + tsscmp@1.0.6: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + tty-browserify@0.0.0: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tunnel@0.0.6: {} + + tw-animate-css@1.4.0: {} + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-detect@4.1.0: {} + + type-fest@0.11.0: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + + type-fest@4.41.0: {} + + type-fest@5.5.0: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typedarray-to-buffer@1.0.4: {} + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typedarray@0.0.6: {} + + typescript-eslint@8.58.0(eslint@8.57.1)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript-eslint@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.37.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript-memoize@1.1.1: {} + + typescript@5.9.3: {} + + ua-parser-js@1.0.41: {} + + uc.micro@1.0.6: {} + + uc.micro@2.1.0: {} + + ufo@1.6.3: {} + + uglify-js@3.19.3: {} + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unc-path-regex@0.1.2: {} + + undefsafe@2.0.5: {} + + underscore.string@3.3.6: + dependencies: + sprintf-js: 1.1.3 + util-deprecate: 1.0.2 + + underscore@1.13.8: {} + + underscore@1.7.0: {} + + underscore@1.8.3: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici-types@7.19.2: {} + + undici-types@7.24.7: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + undici@6.24.1: {} + + undici@7.24.6: {} + + unherit@1.1.3: + dependencies: + inherits: 2.0.4 + xtend: 4.0.2 + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unicorn-magic@0.3.0: {} + + unidecode@0.1.8: {} + + unidecode@1.1.0: {} + + unified@8.4.2: + dependencies: + '@types/unist': 2.0.11 + bail: 1.0.5 + extend: 3.0.2 + is-plain-obj: 2.1.0 + trough: 1.0.5 + vfile: 4.2.1 + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + uniq@1.0.1: {} + + uniqs@2.0.0: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + unist-util-is@3.0.0: {} + + unist-util-is@4.1.0: {} + + unist-util-remove-position@1.1.4: + dependencies: + unist-util-visit: 1.4.1 + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-visit-parents@2.1.2: + dependencies: + unist-util-is: 3.0.0 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit@1.4.1: + dependencies: + unist-util-visit-parents: 2.1.2 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + universalify@0.1.2: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@1.16.1: + dependencies: + acorn: 8.16.0 + webpack-virtual-modules: 0.6.2 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + unquote@1.1.1: {} + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + until-async@3.0.2: {} + + untildify@2.1.0: + dependencies: + os-homedir: 1.0.2 + + upath@1.2.0: + optional: true + + upath@2.0.1: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + upper-case@1.1.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urix@0.1.0: {} + + url-join@4.0.1: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + url-regex-safe@4.0.0(re2@1.23.3): + dependencies: + ip-regex: 4.3.0 + tlds: 1.261.0 + optionalDependencies: + re2: 1.23.3 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.15.0 + + use-callback-ref@1.3.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-debounce@10.1.1(react@18.3.1): + dependencies: + react: 18.3.1 + + use-isomorphic-layout-effect@1.2.1(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-resize-observer@9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@juggle/resize-observer': 3.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + use-sidecar@1.1.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sync-external-store@1.6.0(react@17.0.2): + dependencies: + react: 17.0.2 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use@3.1.1: {} + + username-sync@1.0.3: {} + + util-deprecate@1.0.2: {} + + util.promisify@1.0.1: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.1 + has-symbols: 1.1.0 + object.getownpropertydescriptors: 2.1.9 + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + util@0.11.1: + dependencies: + inherits: 2.0.3 + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.2 + is-typed-array: 1.1.15 + which-typed-array: 1.1.20 + + utils-merge@1.0.1: {} + + uuid@10.0.0: {} + + uuid@3.4.0: {} + + uuid@7.0.3: {} + + uuid@8.3.2: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-compile-cache@2.4.0: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + v8flags@3.2.0: + dependencies: + homedir-polyfill: 1.0.3 + + valid-data-url@3.0.1: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@3.0.0: + dependencies: + builtins: 1.0.3 + + validate-peer-dependencies@1.2.0: + dependencies: + resolve-package-path: 3.1.0 + semver: 7.7.4 + + validator@13.12.0: {} + + validator@7.2.0: {} + + value-or-function@4.0.0: {} + + vary@1.1.2: {} + + velocity-animate@1.5.2: {} + + vendors@1.0.4: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + vfile-location@2.0.6: {} + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + video-extensions@1.2.0: {} + + vinyl-contents@2.0.0: + dependencies: + bl: 5.1.0 + vinyl: 3.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + vinyl-fs@4.0.2: + dependencies: + fs-mkdirp-stream: 2.0.1 + glob-stream: 8.0.3 + graceful-fs: 4.2.11 + iconv-lite: 0.6.3 + is-valid-glob: 1.0.0 + lead: 4.0.0 + normalize-path: 3.0.0 + resolve-options: 2.0.0 + stream-composer: 1.0.2 + streamx: 2.25.0 + to-through: 3.0.0 + value-or-function: 4.0.0 + vinyl: 3.0.1 + vinyl-sourcemap: 2.0.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + vinyl-sourcemap@2.0.0: + dependencies: + convert-source-map: 2.0.0 + graceful-fs: 4.2.11 + now-and-later: 3.0.0 + streamx: 2.25.0 + vinyl: 3.0.1 + vinyl-contents: 2.0.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + vinyl@3.0.1: + dependencies: + clone: 2.1.2 + remove-trailing-separator: 1.1.0 + replace-ext: 2.0.0 + teex: 1.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + vite-node@1.6.1(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@5.5.0) + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@1.6.1(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@5.5.0) + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@3.2.4(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-css-injected-by-js@3.5.2(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + vite-plugin-css-injected-by-js@3.5.2(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + + vite-plugin-svgr@3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + vite-plugin-svgr@3.3.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + vite-plugin-svgr@4.5.0(rollup@4.60.0)(typescript@5.9.3)(vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.0) + '@svgr/core': 8.1.0(typescript@5.9.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + debug: 4.4.3(supports-color@5.5.0) + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + - typescript + + vite@5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.60.0 + optionalDependencies: + '@types/node': 22.19.17 + fsevents: 2.3.3 + less: 4.6.4 + lightningcss: 1.31.1 + terser: 5.46.1 + + vite@5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.60.0 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + less: 4.6.4 + lightningcss: 1.31.1 + terser: 5.46.1 + + vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.6 + rollup: 4.60.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.6.4 + lightningcss: 1.31.1 + terser: 5.46.1 + tsx: 4.21.0 + yaml: 2.8.3 + + vitest@1.6.1(@types/node@22.19.17)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.5 + chai: 4.5.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-node: 1.6.1(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + jsdom: 28.1.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@1.6.1(@types/node@22.19.17)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.5 + chai: 4.5.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-node: 1.6.1(@types/node@22.19.17)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + jsdom: 29.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@1.6.1(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.5 + chai: 4.5.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-node: 1.6.1(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + jsdom: 28.1.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@1.6.1(@types/node@25.6.0)(jsdom@29.0.1(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.5 + chai: 4.5.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + vite-node: 1.6.1(@types/node@25.6.0)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + jsdom: 29.0.1(@noble/hashes@1.8.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@3.2.4(@types/node@25.6.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@1.8.0))(less@4.6.4)(lightningcss@1.31.1)(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3(supports-color@5.5.0) + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 28.1.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@4.1.2(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@25.6.0)(typescript@5.9.3))(vite@7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.1.12(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.31.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.6.0 + jsdom: 28.1.0(@noble/hashes@1.8.0) + transitivePeerDependencies: + - msw + + vm-browserify@1.1.2: {} + + vue-template-compiler@2.7.16: + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + w3c-hr-time@1.0.2: + dependencies: + browser-process-hrtime: 1.0.0 + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@2.0.0: + dependencies: + xml-name-validator: 3.0.0 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + walk-sync@0.3.4: + dependencies: + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 + + walk-sync@1.1.4: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 1.1.2 + + walk-sync@2.2.0: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.5 + + walk-sync@3.0.0: + dependencies: + '@types/minimatch': 3.0.5 + ensure-posix-path: 1.1.1 + matcher-collection: 2.0.1 + minimatch: 3.1.5 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watch-detector@1.0.2: + dependencies: + heimdalljs-logger: 0.1.10 + silent-error: 1.1.1 + tmp: 0.1.0 + transitivePeerDependencies: + - supports-color + + watchpack-chokidar2@2.0.1: + dependencies: + chokidar: 2.1.8 + transitivePeerDependencies: + - supports-color + optional: true + + watchpack@1.7.5: + dependencies: + graceful-fs: 4.2.11 + neo-async: 2.6.2 + optionalDependencies: + chokidar: 3.6.0 + watchpack-chokidar2: 2.0.1 + transitivePeerDependencies: + - supports-color + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-resource-inliner@6.0.1(encoding@0.1.13): + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0(encoding@0.1.13) + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + + webidl-conversions@3.0.1: {} + + webidl-conversions@5.0.0: {} + + webidl-conversions@6.1.0: {} + + webidl-conversions@8.0.1: {} + + webpack-sources@1.4.3: + dependencies: + source-list-map: 2.0.1 + source-map: 0.6.1 + + webpack-sources@3.3.4: {} + + webpack-virtual-modules@0.6.2: {} + + webpack@4.47.0: + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/wasm-edit': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + acorn: 6.4.2 + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + chrome-trace-event: 1.0.4 + enhanced-resolve: 4.5.0 + eslint-scope: 4.0.3 + json-parse-better-errors: 1.0.2 + loader-runner: 2.4.0 + loader-utils: 1.4.2 + memory-fs: 0.4.1 + micromatch: 3.1.10 + mkdirp: 0.5.6 + neo-async: 2.6.2 + node-libs-browser: 2.2.1 + schema-utils: 1.0.0 + tapable: 1.1.3 + terser-webpack-plugin: 1.4.6(webpack@4.47.0) + watchpack: 1.7.5 + webpack-sources: 1.4.3 + transitivePeerDependencies: + - supports-color + + webpack@5.105.4(@swc/core@1.15.21): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(@swc/core@1.15.21)(webpack@5.105.4(@swc/core@1.15.21)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(@swc/core@1.15.21)(esbuild@0.25.12)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.25.12)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(@swc/core@1.15.21)(esbuild@0.27.4)(webpack@5.105.4(@swc/core@1.15.21)(esbuild@0.27.4)) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-encoding@1.0.5: + dependencies: + iconv-lite: 0.4.24 + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@2.3.0: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + whatwg-url@16.0.1(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + whatwg-url@8.7.0: + dependencies: + lodash: 4.17.23 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + whoops@4.1.8: + dependencies: + clean-stack: 3.0.1 + mimic-fn: 3.1.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + word-wrap@1.2.5: {} + + wordwrap@0.0.3: {} + + wordwrap@1.0.0: {} + + worker-farm@1.7.0: + dependencies: + errno: 0.1.8 + + workerpool@2.3.4: + dependencies: + object-assign: 4.1.1 + + workerpool@3.1.2: + dependencies: + '@babel/core': 7.29.0 + object-assign: 4.1.1 + rsvp: 4.8.5 + transitivePeerDependencies: + - supports-color + + workerpool@6.5.1: {} + + workerpool@9.3.4: {} + + world-countries@5.1.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrap-legacy-hbs-plugin-if-needed@1.0.1: + dependencies: + '@glimmer/reference': 0.42.2 + '@glimmer/runtime': 0.42.2 + '@glimmer/syntax': 0.42.2 + '@simple-dom/interface': 1.4.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@7.5.10: {} + + ws@8.18.3: {} + + ws@8.20.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xdg-basedir@4.0.0: {} + + xml-name-validator@3.0.0: {} + + xml-name-validator@5.0.0: {} + + xml@1.0.1: {} + + xmlchars@2.2.0: {} + + xregexp@2.0.0: {} + + xtend@2.0.6: + dependencies: + is-object: 0.1.2 + object-keys: 0.2.0 + + xtend@2.1.2: + dependencies: + object-keys: 0.4.0 + + xtend@2.2.0: {} + + xtend@3.0.0: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yam@1.0.0: + dependencies: + fs-extra: 4.0.3 + lodash.merge: 4.6.2 + + yaml@1.10.3: {} + + yaml@2.8.3: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + yup@0.32.9: + dependencies: + '@babel/runtime': 7.29.2 + '@types/lodash': 4.17.24 + lodash: 4.17.23 + lodash-es: 4.18.1 + nanoclone: 0.2.1 + property-expr: 2.0.6 + toposort: 2.0.2 + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + + zod@4.1.12: {} + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..b3c5209 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,15 @@ +packages: + - 'ghost/*' + - 'apps/*' + - 'e2e' + +strictDepBuilds: true + +catalog: + '@eslint/js': 8.57.1 + eslint: 8.57.1 + +catalogs: + eslint9: + '@eslint/js': 9.37.0 + eslint: 9.37.0 diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js new file mode 100644 index 0000000..68e8a65 --- /dev/null +++ b/scripts/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + ecmaVersion: 2022 + }, + env: { + node: true, + es2022: true + } +}; diff --git a/scripts/lib/release-notes.js b/scripts/lib/release-notes.js new file mode 100644 index 0000000..fdc4d9c --- /dev/null +++ b/scripts/lib/release-notes.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node +'use strict'; + +const {execSync} = require('node:child_process'); +const path = require('node:path'); + +const ROOT = path.resolve(__dirname, '../..'); +const REPO_URL = 'https://github.com/TryGhost/Ghost'; + +// Emoji priority order (lowest index = lowest priority, sorted descending) +const EMOJI_ORDER = ['💡', '🐛', '🎨', '💄', '✨', '🔒']; + +// User-facing emojis — only these are included in release notes +const USER_FACING_EMOJIS = new Set(EMOJI_ORDER); + +function getCommitLog(fromTag, toTag) { + const range = `${fromTag}..${toTag}`; + const format = '* %s - %an'; + const cmd = `git log --no-merges --pretty=tformat:'${format}' ${range}`; + + let log; + try { + log = execSync(cmd, {cwd: ROOT, encoding: 'utf8'}).trim(); + } catch { + return []; + } + + if (!log) { + return []; + } + + return log.split('\n').map(line => line.trim()); +} + +function extractLeadingEmoji(line) { + // Line format: * - + const match = line.match(/^\* (.)/u); + return match ? match[1] : ''; +} + +function filterAndSortByEmoji(lines) { + const emojiLines = lines.filter((line) => { + const emoji = extractLeadingEmoji(line); + return USER_FACING_EMOJIS.has(emoji); + }); + + emojiLines.sort((a, b) => { + const emojiA = extractLeadingEmoji(a); + const emojiB = extractLeadingEmoji(b); + const indexA = EMOJI_ORDER.indexOf(emojiA); + const indexB = EMOJI_ORDER.indexOf(emojiB); + return indexB - indexA; + }); + + return emojiLines; +} + +function generateReleaseNotes(fromTag, toTag) { + const lines = getCommitLog(fromTag, toTag); + const filtered = filterAndSortByEmoji(lines); + + let body; + if (filtered.length === 0) { + body = 'This release contains fixes for minor bugs and issues reported by Ghost users.'; + } else { + // Deduplicate (preserving order) + body = [...new Set(filtered)].join('\n'); + } + + body += `\n\n---\n\nView the changelog for full details: ${REPO_URL}/compare/${fromTag}...${toTag}`; + + return body; +} + +// CLI: node release-notes.js +if (require.main === module) { + const [fromTag, toTag] = process.argv.slice(2); + + if (!fromTag || !toTag) { + console.error('Usage: node release-notes.js '); + process.exit(1); + } + + process.stdout.write(generateReleaseNotes(fromTag, toTag)); +} + +module.exports = {generateReleaseNotes}; diff --git a/scripts/lib/resolve-base-tag.js b/scripts/lib/resolve-base-tag.js new file mode 100644 index 0000000..7c81c60 --- /dev/null +++ b/scripts/lib/resolve-base-tag.js @@ -0,0 +1,31 @@ +const semver = require('semver'); +const {execSync} = require('node:child_process'); + +/** + * Resolve the base git tag for diff/log comparisons during release preparation. + * + * For stable versions (e.g. "6.18.0"), returns "v6.18.0" — the tag for that version. + * For prerelease versions (e.g. "6.19.0-rc.0"), the tag "v6.19.0-rc.0" won't exist, + * so we find the most recent stable version tag in HEAD's ancestry using git describe. + * + * @param {string} version - The current Ghost version from package.json + * @param {string} repoDir - Path to the Ghost repo checkout + * @returns {{tag: string, isPrerelease: boolean}} + */ +function resolveBaseTag(version, repoDir) { + if (semver.prerelease(version)) { + const tag = execSync( + `git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' --exclude 'v*-*' HEAD`, + {cwd: repoDir, encoding: 'utf8'} + ).trim(); + + return {tag, isPrerelease: true}; + } + + return { + tag: `v${version}`, + isPrerelease: false + }; +} + +module.exports = {resolveBaseTag}; diff --git a/scripts/release.js b/scripts/release.js new file mode 100644 index 0000000..11c759f --- /dev/null +++ b/scripts/release.js @@ -0,0 +1,326 @@ +#!/usr/bin/env node +'use strict'; + +const path = require('node:path'); +const fs = require('node:fs'); +const {execSync} = require('node:child_process'); +const semver = require('semver'); +const {resolveBaseTag} = require('./lib/resolve-base-tag'); + +const ROOT = path.resolve(__dirname, '..'); +const GHOST_CORE_PKG = path.join(ROOT, 'ghost/core/package.json'); +const GHOST_ADMIN_PKG = path.join(ROOT, 'ghost/admin/package.json'); +const CASPER_DIR = path.join(ROOT, 'ghost/core/content/themes/casper'); +const SOURCE_DIR = path.join(ROOT, 'ghost/core/content/themes/source'); + +const MAX_WAIT_MS = 30 * 60 * 1000; // 30 minutes +const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds + +// --- Argument parsing --- + +function parseArgs() { + const args = process.argv.slice(2); + const opts = { + bumpType: 'auto', + branch: 'main', + dryRun: false, + skipChecks: false + }; + + for (const arg of args) { + if (arg.startsWith('--bump-type=')) { + opts.bumpType = arg.split('=')[1]; + } else if (arg.startsWith('--branch=')) { + opts.branch = arg.split('=')[1]; + } else if (arg === '--dry-run') { + opts.dryRun = true; + } else if (arg === '--skip-checks') { + opts.skipChecks = true; + } else { + console.error(`Unknown argument: ${arg}`); + process.exit(1); + } + } + + return opts; +} + +// --- Helpers --- + +function run(cmd, opts = {}) { + const result = execSync(cmd, {cwd: ROOT, encoding: 'utf8', ...opts}); + return result.trim(); +} + +function readPkgVersion(pkgPath) { + return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; +} + +function writePkgVersion(pkgPath, version) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + pkg.version = version; + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} + +function log(msg) { + console.log(` ${msg}`); +} + +function logStep(msg) { + console.log(`\n▸ ${msg}`); +} + +// --- Version detection --- + +function detectBumpType(baseTag, bumpType) { + // Check for new migration files + const migrationsPath = 'ghost/core/core/server/data/migrations/versions/'; + try { + const addedFiles = run(`git diff --diff-filter=A --name-only ${baseTag} HEAD -- ${migrationsPath}`); + if (addedFiles?.includes('core/')) { + log('New migrations detected'); + if (bumpType === 'auto') { + log('Auto-detecting: bumping to minor'); + bumpType = 'minor'; + } + } else { + log('No new migrations detected'); + } + } catch { + log('Warning: could not diff migrations'); + } + + // Check for feature commits (✨ or 🎉) + try { + const commits = run(`git log --oneline ${baseTag}..HEAD`); + if (commits) { + const featureCommits = commits.split('\n').filter(c => c.includes('✨') || c.includes('🎉') || c.includes(':sparkles:')); + if (featureCommits.length) { + log(`Feature commits detected (${featureCommits.length})`); + if (bumpType === 'auto') { + log('Auto-detecting: bumping to minor'); + bumpType = 'minor'; + } + } else { + log('No feature commits detected'); + } + } else { + log('No commits since base tag'); + } + } catch { + log('Warning: could not read commit log'); + } + + if (bumpType === 'auto') { + log('Defaulting to patch'); + bumpType = 'patch'; + } + + return bumpType; +} + +// --- CI check polling --- + +const REQUIRED_CHECK_NAME = 'All required tests passed or skipped'; + +async function waitForChecks(commit) { + logStep(`Waiting for CI checks on ${commit.slice(0, 8)}...`); + + const token = process.env.GITHUB_TOKEN || process.env.RELEASE_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN or RELEASE_TOKEN required for check polling'); + } + + const startTime = Date.now(); + + while (true) { // eslint-disable-line no-constant-condition + const response = await fetch(`https://api.github.com/repos/TryGhost/Ghost/commits/${commit}/check-runs`, { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github+json' + } + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + const {check_runs: checkRuns} = await response.json(); + + // Find the required check — this is the CI gate that aggregates all mandatory checks + const requiredCheck = checkRuns.find(r => r.name === REQUIRED_CHECK_NAME); + + if (requiredCheck) { + if (requiredCheck.status === 'completed' && requiredCheck.conclusion === 'success') { + log(`Required check "${REQUIRED_CHECK_NAME}" passed`); + return; + } + if (requiredCheck.status === 'completed' && requiredCheck.conclusion !== 'success') { + throw new Error(`Required check "${REQUIRED_CHECK_NAME}" failed (${requiredCheck.conclusion})`); + } + log(`Required check is ${requiredCheck.status}, waiting...`); + } else { + log('Required check not found yet, waiting...'); + } + + const elapsedMs = Date.now() - startTime; + const elapsed = Math.round(elapsedMs / 1000); + + if (elapsedMs >= MAX_WAIT_MS) { + throw new Error(`Timed out waiting for "${REQUIRED_CHECK_NAME}" after ${elapsed}s`); + } + + log(`(${elapsed}s elapsed), polling in 30s...`); + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); + } +} + +// --- Theme submodule updates --- + +function updateThemeSubmodule(themeDir, themeName) { + if (!fs.existsSync(themeDir)) { + log(`${themeName} not present, skipping`); + return false; + } + + const currentPkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8')); + const currentVersion = currentPkg.version; + + // Checkout latest stable tag on main branch + try { + execSync( + `git checkout $(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1 --branches=main))`, + {cwd: themeDir, encoding: 'utf8', stdio: 'pipe'} + ); + } catch (err) { + log(`Warning: failed to update ${themeName}: ${err.message}`); + return false; + } + + const updatedPkg = JSON.parse(fs.readFileSync(path.join(themeDir, 'package.json'), 'utf8')); + const newVersion = updatedPkg.version; + + if (semver.gt(newVersion, currentVersion)) { + log(`${themeName} updated: v${currentVersion} → v${newVersion}`); + run(`git add -f ${path.relative(ROOT, themeDir)}`); + run(`git commit -m "🎨 Updated ${themeName} to v${newVersion}"`); + return true; + } + + log(`${themeName} already at latest (v${currentVersion})`); + return false; +} + +// --- Main --- + +async function main() { + const opts = parseArgs(); + + console.log('Ghost Release Script'); + console.log('===================='); + log(`Branch: ${opts.branch}`); + log(`Bump type: ${opts.bumpType}`); + log(`Dry run: ${opts.dryRun}`); + + // 1. Read current version + logStep('Reading current version'); + const currentVersion = readPkgVersion(GHOST_CORE_PKG); + log(`Current version: ${currentVersion}`); + + // 2. Resolve base tag + logStep('Resolving base tag'); + const {tag: baseTag, isPrerelease} = resolveBaseTag(currentVersion, ROOT); + if (isPrerelease) { + log(`Prerelease detected (${currentVersion}), resolved base tag: ${baseTag}`); + } else { + log(`Base tag: ${baseTag}`); + } + + // 3. Detect bump type + logStep('Detecting bump type'); + const resolvedBumpType = detectBumpType(baseTag, opts.bumpType); + const newVersion = semver.inc(currentVersion, resolvedBumpType); + if (!newVersion) { + console.error(`Failed to calculate new version from ${currentVersion} with bump type ${resolvedBumpType}`); + process.exit(1); + } + log(`Bump type: ${resolvedBumpType}`); + log(`New version: ${newVersion}`); + + // 4. Check tag doesn't exist + logStep('Checking remote tags'); + try { + const tagCheck = run(`git ls-remote --tags origin refs/tags/v${newVersion}`); + if (tagCheck) { + console.error(`Tag v${newVersion} already exists on remote. Cannot release this version.`); + process.exit(1); + } + } catch { + // ls-remote returns non-zero if no match — that's what we want + } + log(`Tag v${newVersion} does not exist on remote`); + + // 5. Wait for CI checks + if (!opts.skipChecks) { + const headSha = run('git rev-parse HEAD'); + await waitForChecks(headSha); + } else { + log('Skipping CI checks'); + } + + // 6. Update theme submodules (main branch only) + if (opts.branch === 'main') { + logStep('Updating theme submodules'); + run('git submodule update --init'); + updateThemeSubmodule(CASPER_DIR, 'Casper'); + updateThemeSubmodule(SOURCE_DIR, 'Source'); + } else { + logStep('Skipping theme updates (not main branch)'); + } + + // 7. Bump versions + logStep(`Bumping version to ${newVersion}`); + writePkgVersion(GHOST_CORE_PKG, newVersion); + writePkgVersion(GHOST_ADMIN_PKG, newVersion); + + // 8. Commit and tag + run(`git add ${path.relative(ROOT, GHOST_CORE_PKG)} ${path.relative(ROOT, GHOST_ADMIN_PKG)}`); + run(`git commit -m "v${newVersion}"`); + run(`git tag v${newVersion}`); + log(`Created tag v${newVersion}`); + + // 9. Push + if (opts.dryRun) { + logStep('DRY RUN — skipping push'); + log(`Would push branch ${opts.branch} and tag v${newVersion}`); + } else { + logStep('Pushing'); + run('git push origin HEAD'); + run(`git push origin v${newVersion}`); + log('Pushed branch and tag'); + } + + // 10. Advance to next RC + logStep('Advancing to next RC'); + const nextMinor = semver.inc(newVersion, 'minor'); + const nextRc = `${nextMinor}-rc.0`; + log(`Next RC: ${nextRc}`); + writePkgVersion(GHOST_CORE_PKG, nextRc); + writePkgVersion(GHOST_ADMIN_PKG, nextRc); + run(`git add ${path.relative(ROOT, GHOST_CORE_PKG)} ${path.relative(ROOT, GHOST_ADMIN_PKG)}`); + run(`git commit -m "Bumped version to ${nextRc}"`); + + if (opts.dryRun) { + log('DRY RUN — skipping RC push'); + } else { + run('git push origin HEAD'); + log('Pushed RC version'); + } + + console.log(`\n✓ Release ${newVersion} complete`); +} + +main().catch((err) => { + console.error(`\n✗ Release failed: ${err.message}`); + process.exit(1); +}); diff --git a/scripts/test/resolve-base-tag.test.js b/scripts/test/resolve-base-tag.test.js new file mode 100644 index 0000000..9dbf6b0 --- /dev/null +++ b/scripts/test/resolve-base-tag.test.js @@ -0,0 +1,179 @@ +const {describe, it, before, after} = require('node:test'); +const assert = require('node:assert'); +const os = require('node:os'); +const path = require('node:path'); +const fs = require('node:fs'); +const {execSync} = require('node:child_process'); +const semver = require('semver'); +const {resolveBaseTag} = require('../lib/resolve-base-tag'); + +/** + * Create a temporary git repo with semver tags for testing. + * Returns the repo path. Caller is responsible for cleanup. + */ +function createTestRepo() { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghost-release-test-')); + + execSync([ + `cd ${tmpDir}`, + 'git init', + 'git config user.email "test@test.com"', + 'git config user.name "Test"', + + // Initial commit + stable tag v6.17.0 + 'echo "initial" > file.txt', + 'git add .', + 'git commit -m "initial"', + 'git tag v6.17.0', + + // Second commit + stable tag v6.17.1 + 'echo "patch" >> file.txt', + 'git add .', + 'git commit -m "patch release"', + 'git tag v6.17.1', + + // Third commit (no tag — simulates post-release development) + 'echo "new work" >> file.txt', + 'git add .', + 'git commit -m "post-release commit"' + ].join(' && ')); + + return tmpDir; +} + +describe('resolveBaseTag', () => { + let testRepo; + + before(() => { + testRepo = createTestRepo(); + }); + + after(() => { + fs.rmSync(testRepo, {recursive: true, force: true}); + }); + + describe('semver.prerelease detection', () => { + it('returns null for stable versions', () => { + assert.strictEqual(semver.prerelease('6.17.1'), null); + assert.strictEqual(semver.prerelease('6.18.0'), null); + assert.strictEqual(semver.prerelease('5.0.0'), null); + }); + + it('returns prerelease components for rc versions', () => { + const result = semver.prerelease('6.19.0-rc.0'); + assert.deepStrictEqual(result, ['rc', 0]); + }); + + it('returns prerelease components for alpha versions', () => { + const result = semver.prerelease('7.0.0-alpha.1'); + assert.deepStrictEqual(result, ['alpha', 1]); + }); + }); + + describe('stable version path', () => { + it('constructs tag directly from version', () => { + // Stable versions don't hit git — the tag is just v{version} + const {tag, isPrerelease} = resolveBaseTag('6.17.1', '/nonexistent'); + assert.strictEqual(tag, 'v6.17.1'); + assert.strictEqual(isPrerelease, false); + }); + + it('works for any stable version format', () => { + const {tag} = resolveBaseTag('5.0.0', '/nonexistent'); + assert.strictEqual(tag, 'v5.0.0'); + }); + }); + + describe('prerelease version path', () => { + it('resolves to the latest stable tag', () => { + const {tag, isPrerelease} = resolveBaseTag('6.19.0-rc.0', testRepo); + assert.strictEqual(isPrerelease, true); + assert.strictEqual(tag, 'v6.17.1'); + }); + + it('resolved tag is a stable version (no prerelease suffix)', () => { + const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo); + assert.match(tag, /^v\d+\.\d+\.\d+$/); + assert.strictEqual(semver.prerelease(tag.slice(1)), null); + }); + + it('resolved tag is a valid git ref', () => { + const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo); + const sha = execSync(`cd ${testRepo} && git rev-parse ${tag}`, {encoding: 'utf8'}); + assert.ok(sha.trim().length > 0, 'Tag should resolve to a commit SHA'); + }); + + it('git diff succeeds with the resolved tag', () => { + const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo); + const result = execSync( + `cd ${testRepo} && git diff --diff-filter=A --name-only ${tag} HEAD`, + {encoding: 'utf8'} + ); + assert.strictEqual(typeof result, 'string'); + }); + + it('git log succeeds with the resolved tag', () => { + const {tag} = resolveBaseTag('6.19.0-rc.0', testRepo); + const result = execSync( + `cd ${testRepo} && git log --oneline ${tag}..HEAD`, + {encoding: 'utf8'} + ); + assert.strictEqual(typeof result, 'string'); + // Should have at least 1 commit (the post-release commit) + assert.ok(result.trim().length > 0, 'Should find commits after the tag'); + }); + }); + + describe('prerelease excludes prerelease tags', () => { + let repoWithPrereleaseTag; + + before(() => { + // Create a repo that has both stable and prerelease tags + repoWithPrereleaseTag = fs.mkdtempSync(path.join(os.tmpdir(), 'ghost-release-test-pre-')); + execSync([ + `cd ${repoWithPrereleaseTag}`, + 'git init', + 'git config user.email "test@test.com"', + 'git config user.name "Test"', + + 'echo "initial" > file.txt', + 'git add .', + 'git commit -m "initial"', + 'git tag v6.17.0', + + 'echo "alpha" >> file.txt', + 'git add .', + 'git commit -m "alpha work"', + 'git tag v7.0.0-alpha.0', + + 'echo "more" >> file.txt', + 'git add .', + 'git commit -m "more work"' + ].join(' && ')); + }); + + after(() => { + fs.rmSync(repoWithPrereleaseTag, {recursive: true, force: true}); + }); + + it('skips prerelease tags and finds the stable one', () => { + const {tag} = resolveBaseTag('7.0.0-rc.0', repoWithPrereleaseTag); + // Should find v6.17.0 (stable), NOT v7.0.0-alpha.0 (prerelease) + assert.strictEqual(tag, 'v6.17.0'); + }); + }); + + describe('semver.inc compatibility', () => { + it('patch of an rc produces the stable release', () => { + assert.strictEqual(semver.inc('6.19.0-rc.0', 'patch'), '6.19.0'); + }); + + it('minor of an rc produces the stable release (not next minor)', () => { + assert.strictEqual(semver.inc('6.19.0-rc.0', 'minor'), '6.19.0'); + }); + + it('prerelease rc of an rc increments the rc number', () => { + assert.strictEqual(semver.inc('6.19.0-rc.0', 'prerelease', 'rc'), '6.19.0-rc.1'); + }); + }); +}); diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..4ccda89 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "skills": { + "Add Admin API Endpoint": { + "source": "./.agents", + "sourceType": "local", + "computedHash": "a7b2f6474e6e7d4ec688ab311ecc46390fe4d0eefe871f3e2d2f01edb1ed9684" + }, + "Create database migration": { + "source": "./.agents", + "sourceType": "local", + "computedHash": "d547f2d90a3cc0bde9a9458847c07c313e7e5afd093c8c1e719dc06c63b38375" + }, + "Format numbers": { + "source": "./.agents", + "sourceType": "local", + "computedHash": "63f72b2d778752aabb9bd4109ef83b80262428964cd9498ad3631760132ceeff" + }, + "add-private-feature-flag": { + "source": "./.agents", + "sourceType": "local", + "computedHash": "ecdbd8c0832ed9be10573d3c7383291ae0b0a6ab84fa496b4a0ed54aac8d176b" + }, + "commit": { + "source": "./.agents", + "sourceType": "local", + "computedHash": "f2f9a52e210a32ad42d4bf5826ce7cec265a2c472de03bee3840a5b295d734d7" + } + } +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..874fc4d --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,15 @@ +# Ignore SonarCloud rule docker:S8431 ("Use either the version tag or the +# digest for the image instead of both") for all Dockerfiles. +# +# Renovate's default behaviour when pinning Docker images is to keep the +# human-readable tag and append the immutable digest, producing refs of +# the form `image:tag@sha256:...`. This is the format documented by +# Renovate and is what `pinDigest` updates produce out of the box. +# +# SonarCloud's docker:S8431 rule flags that format as a maintainability +# warning. As it's pure maintainability, we want to prefer renovate's +# default behavour to keep automerging working, which is actually more +# maintainable for us. +sonar.issue.ignore.multicriteria=e1 +sonar.issue.ignore.multicriteria.e1.ruleKey=docker:S8431 +sonar.issue.ignore.multicriteria.e1.resourceKey=**/Dockerfile*