diff --git a/devex-ui/.gitignore b/devex-ui/.gitignore index b8b48746..82e5f985 100644 --- a/devex-ui/.gitignore +++ b/devex-ui/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? pull.sh +.vercel diff --git a/devex-ui/api/ssr.js b/devex-ui/api/ssr.js new file mode 100644 index 00000000..10f657ae --- /dev/null +++ b/devex-ui/api/ssr.js @@ -0,0 +1,52 @@ +// api/ssr.js +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { render } from '../dist/server/entry-server.js'; + +// Get the directory name +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default async function handler(req, res) { + try { + const url = req.url; + + // Check if this route should be server-side rendered + const isDocsRoute = url.startsWith('/docs'); + const isWelcomeRoute = url === '/welcome'; + const defaultPage = process.env.VITE_DEFAULT_PAGE || 'devx'; + const shouldApplySSR = isDocsRoute || (isWelcomeRoute && defaultPage === 'welcome'); + + if (!shouldApplySSR) { + // For routes that don't need SSR, just serve the index.html + const indexHtml = fs.readFileSync( + path.resolve(__dirname, '../dist/client/index.html'), + 'utf-8' + ); + return res.status(200).setHeader('Content-Type', 'text/html').send(indexHtml); + } + + // Get the template + const template = fs.readFileSync( + path.resolve(__dirname, '../dist/client/index.html'), + 'utf-8' + ); + + // Render the app + const context = {}; + const { html: appHtml } = await render(url, context); + + // If the context has a URL property, that means there was a redirect + if (context.url) { + return res.redirect(301, context.url); + } + + // Replace the placeholder with the app HTML + const html = template.replace('', appHtml); + + return res.status(200).setHeader('Content-Type', 'text/html').send(html); + } catch (e) { + console.error(e); + return res.status(500).send(e.message); + } +} \ No newline at end of file diff --git a/devex-ui/api/vercel.js b/devex-ui/api/vercel.js new file mode 100644 index 00000000..7adb8685 --- /dev/null +++ b/devex-ui/api/vercel.js @@ -0,0 +1,4 @@ +// api/vercel.js +import { render } from '../dist/server/entry-server.js'; + +export { render }; \ No newline at end of file diff --git a/devex-ui/docs b/devex-ui/docs new file mode 120000 index 00000000..a9594bfe --- /dev/null +++ b/devex-ui/docs @@ -0,0 +1 @@ +../docs \ No newline at end of file diff --git a/devex-ui/docs/API.md b/devex-ui/docs/API.md deleted file mode 100644 index ef1f8e7d..00000000 --- a/devex-ui/docs/API.md +++ /dev/null @@ -1,530 +0,0 @@ - -# API Integration Documentation - -## Overview - -This document provides comprehensive guidance for integrating APIs and implementing real-time features using WebSockets in the Domain Development Navigator application. - -## Table of Contents - -1. [Data Contracts](#data-contracts) -2. [REST API Integration](#rest-api-integration) -3. [WebSocket Live Streaming](#websocket-live-streaming) -4. [Event Stream Integration](#event-stream-integration) -5. [Command API](#command-api) -6. [Implementation Examples](#implementation-examples) -7. [Code Locations](#code-locations) - -## Data Contracts - -### Common Types - -```typescript -// src/types/common.ts -export type UUID = string; - -/** - * Common Metadata across Commands and Events - */ -export interface Metadata { - userId?: UUID; //AUTH/RBAC/RLS - role?: string; //RBAC/RLS - timestamp: Date; - correlationId?: UUID; // ID for tracking the flow of actions - causationId?: UUID; // ID of the action that caused this command - requestId?: string; // Useful for cross-service tracing - source?: string; // Service or workflow origin - tags?: Record; // For flexible enrichment - schemaVersion?: number; -} - -/** - * Base Command interface with lifecycle hints - */ -export interface Command { - id: UUID; - tenant_id: UUID; - type: string; - payload: T; - status?: 'pending' | 'consumed' | 'processed' | 'failed'; - metadata?: Metadata; -} - -/** - * Base Event interface with versioning and full trace metadata - */ -export interface Event { - id: UUID; - tenant_id: UUID; - type: string; - payload: T; - aggregateId: UUID; - aggregateType: string; - version: number; - metadata?: Metadata; -} -``` - -## REST API Integration - -### Basic API Service Structure - -```typescript -// src/services/apiService.ts -export class ApiService { - private baseUrl: string; - private headers: Record; - - constructor(baseUrl: string, apiKey?: string) { - this.baseUrl = baseUrl; - this.headers = { - 'Content-Type': 'application/json', - ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }) - }; - } - - async get(endpoint: string): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - method: 'GET', - headers: this.headers, - }); - - if (!response.ok) { - throw new Error(`API Error: ${response.status}`); - } - - return response.json(); - } - - async post(endpoint: string, data: any): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - method: 'POST', - headers: this.headers, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error(`API Error: ${response.status}`); - } - - return response.json(); - } -} -``` - -### Environment Configuration - -```typescript -// src/config/api.ts -export const API_CONFIG = { - BASE_URL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', - WS_URL: import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws', - API_KEY: import.meta.env.VITE_API_KEY, - TIMEOUT: 30000, -}; -``` - -## WebSocket Live Streaming - -### WebSocket Service Implementation - -```typescript -// src/services/websocketService.ts -export class WebSocketService { - private ws: WebSocket | null = null; - private reconnectAttempts = 0; - private maxReconnectAttempts = 5; - private reconnectInterval = 1000; - private listeners = new Map>(); - - constructor(private url: string) {} - - connect(): Promise { - return new Promise((resolve, reject) => { - try { - this.ws = new WebSocket(this.url); - - this.ws.onopen = () => { - console.log('WebSocket connected'); - this.reconnectAttempts = 0; - resolve(); - }; - - this.ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - this.handleMessage(data); - } catch (error) { - console.error('Failed to parse WebSocket message:', error); - } - }; - - this.ws.onclose = () => { - console.log('WebSocket disconnected'); - this.handleReconnect(); - }; - - this.ws.onerror = (error) => { - console.error('WebSocket error:', error); - reject(error); - }; - } catch (error) { - reject(error); - } - }); - } - - private handleMessage(data: any) { - const { type, payload } = data; - const typeListeners = this.listeners.get(type); - - if (typeListeners) { - typeListeners.forEach(callback => callback(payload)); - } - } - - private handleReconnect() { - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++; - setTimeout(() => { - console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`); - this.connect(); - }, this.reconnectInterval * this.reconnectAttempts); - } - } - - subscribe(eventType: string, callback: Function) { - if (!this.listeners.has(eventType)) { - this.listeners.set(eventType, new Set()); - } - this.listeners.get(eventType)!.add(callback); - } - - unsubscribe(eventType: string, callback: Function) { - const typeListeners = this.listeners.get(eventType); - if (typeListeners) { - typeListeners.delete(callback); - } - } - - send(data: any) { - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.send(JSON.stringify(data)); - } else { - console.warn('WebSocket is not connected'); - } - } - - disconnect() { - if (this.ws) { - this.ws.close(); - this.ws = null; - } - } -} -``` - -### React Hook for WebSocket - -```typescript -// src/hooks/useWebSocket.ts -import { useEffect, useRef, useState } from 'react'; -import { WebSocketService } from '@/services/websocketService'; - -export const useWebSocket = (url: string) => { - const [isConnected, setIsConnected] = useState(false); - const [error, setError] = useState(null); - const wsService = useRef(null); - - useEffect(() => { - wsService.current = new WebSocketService(url); - - wsService.current.connect() - .then(() => { - setIsConnected(true); - setError(null); - }) - .catch((err) => { - setError(err.message); - setIsConnected(false); - }); - - return () => { - wsService.current?.disconnect(); - }; - }, [url]); - - const subscribe = (eventType: string, callback: Function) => { - wsService.current?.subscribe(eventType, callback); - }; - - const unsubscribe = (eventType: string, callback: Function) => { - wsService.current?.unsubscribe(eventType, callback); - }; - - const send = (data: any) => { - wsService.current?.send(data); - }; - - return { - isConnected, - error, - subscribe, - unsubscribe, - send, - }; -}; -``` - -## Event Stream Integration - -### Event Stream Hook - -```typescript -// src/hooks/useEventStream.ts -import { useState, useEffect, useCallback } from 'react'; -import { useWebSocket } from './useWebSocket'; -import { Event, UUID } from '@/types/common'; - -export const useEventStream = (tenantId: UUID, wsUrl: string) => { - const [events, setEvents] = useState([]); - const [isLive, setIsLive] = useState(true); - const { isConnected, subscribe, unsubscribe, send } = useWebSocket(wsUrl); - - const handleNewEvent = useCallback((eventData: Event) => { - if (eventData.tenant_id === tenantId && isLive) { - setEvents(prev => [eventData, ...prev].slice(0, 100)); // Keep latest 100 - } - }, [tenantId, isLive]); - - useEffect(() => { - if (isConnected) { - subscribe('event', handleNewEvent); - - // Subscribe to tenant-specific events - send({ - type: 'subscribe', - payload: { tenant_id: tenantId, eventTypes: ['*'] } - }); - } - - return () => { - unsubscribe('event', handleNewEvent); - }; - }, [isConnected, handleNewEvent, tenantId, subscribe, unsubscribe, send]); - - const toggleLive = () => { - setIsLive(!isLive); - }; - - const clearEvents = () => { - setEvents([]); - }; - - return { - events, - isLive, - isConnected, - toggleLive, - clearEvents, - }; -}; -``` - -## Command API - -### Command Service - -```typescript -// src/services/commandService.ts -import { ApiService } from './apiService'; -import { Command, UUID } from '@/types/common'; - -export interface CommandResult { - commandId: UUID; - status: 'accepted' | 'rejected'; - message?: string; - events?: any[]; -} - -export class CommandService extends ApiService { - async executeCommand(command: Omit): Promise { - return this.post('/commands', command); - } - - async getCommandStatus(commandId: UUID): Promise { - return this.get(`/commands/${commandId}/status`); - } - - async getCommandHistory(tenantId: UUID, limit = 50): Promise { - return this.get(`/commands?tenant_id=${tenantId}&limit=${limit}`); - } -} -``` - -## Implementation Examples - -### Mock Data Generation - -```typescript -// src/utils/mockData.ts -import { Event, Command, Metadata, UUID } from '@/types/common'; - -export const generateMockEvent = (tenantId: UUID, overrides?: Partial): Event => { - const eventTypes = ['UserCreated', 'OrderPlaced', 'PaymentProcessed', 'ProductUpdated']; - const aggregateTypes = ['User', 'Order', 'Payment', 'Product']; - - const metadata: Metadata = { - userId: `user-${Math.random().toString(36).substr(2, 6)}`, - role: 'user', - timestamp: new Date(), - correlationId: `corr-${Math.random().toString(36).substr(2, 8)}`, - causationId: `cmd-${Math.random().toString(36).substr(2, 8)}`, - requestId: `req-${Math.random().toString(36).substr(2, 8)}`, - source: 'web-api', - tags: { environment: 'development' }, - schemaVersion: 1 - }; - - return { - id: `evt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, - tenant_id: tenantId, - type: eventTypes[Math.floor(Math.random() * eventTypes.length)], - payload: { randomData: Math.random(), timestamp: new Date() }, - aggregateId: `agg-${Math.random().toString(36).substr(2, 6)}`, - aggregateType: aggregateTypes[Math.floor(Math.random() * aggregateTypes.length)], - version: Math.floor(Math.random() * 5) + 1, - metadata, - ...overrides - }; -}; - -export const generateMockCommand = (tenantId: UUID, overrides?: Partial): Command => { - const commandTypes = ['CreateUser', 'PlaceOrder', 'ProcessPayment', 'UpdateProduct']; - - const metadata: Metadata = { - userId: `user-${Math.random().toString(36).substr(2, 6)}`, - role: 'admin', - timestamp: new Date(), - correlationId: `corr-${Math.random().toString(36).substr(2, 8)}`, - requestId: `req-${Math.random().toString(36).substr(2, 8)}`, - source: 'navigator-ui', - tags: { environment: 'development' }, - schemaVersion: 1 - }; - - return { - id: `cmd-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, - tenant_id: tenantId, - type: commandTypes[Math.floor(Math.random() * commandTypes.length)], - payload: { randomData: Math.random(), timestamp: new Date() }, - status: 'pending', - metadata, - ...overrides - }; -}; -``` - -## Code Locations - -### Files to Create for API Integration - -1. **Type Definitions** - - Create: `src/types/common.ts` - Base types for Event, Command, Metadata - -2. **Environment Configuration** - - Create: `src/config/api.ts` - API endpoints and configuration - -3. **Service Layer** - - Create: `src/services/apiService.ts` - Base API service - - Create: `src/services/websocketService.ts` - WebSocket management - - Create: `src/services/commandService.ts` - Command API integration - -4. **Hooks** - - Create: `src/hooks/useWebSocket.ts` - WebSocket connection hook - - Create: `src/hooks/useEventStream.ts` - Event streaming hook - - Create: `src/hooks/useCommands.ts` - Command management hook - -5. **Utilities** - - Create: `src/utils/mockData.ts` - Mock data generators - - Create: `src/utils/errorHandler.ts` - Centralized error handling - -6. **Components to Update** - - `src/components/EventStreamViewer.tsx` - Update with new Event interface - - `src/components/CommandIssuer.tsx` - Update with new Command interface - - `src/components/SystemStatus.tsx` - Add real system monitoring - - `src/components/ProjectionExplorer.tsx` - Connect to projection APIs - -### Environment Variables Needed - -```env -VITE_API_BASE_URL=http://localhost:8080 -VITE_WS_URL=ws://localhost:8080/ws -VITE_API_KEY=your-api-key-here -``` - -### Error Handling Strategy - -```typescript -// src/utils/errorHandler.ts -import { toast } from "@/components/ui/use-toast"; - -export const handleApiError = (error: any) => { - console.error('API Error:', error); - - if (error.name === 'NetworkError') { - toast({ - variant: "destructive", - title: "Network Error", - description: "Network connection failed. Please check your connection.", - }); - } else if (error.status === 401) { - toast({ - variant: "destructive", - title: "Authentication Failed", - description: "Authentication failed. Please check your API key.", - }); - } else if (error.status >= 500) { - toast({ - variant: "destructive", - title: "Server Error", - description: "Server error. Please try again later.", - }); - } else { - toast({ - variant: "destructive", - title: "Error", - description: error.message || "An unexpected error occurred.", - }); - } -}; -``` - -## Testing WebSocket Connection - -```typescript -// src/utils/testConnection.ts -export const testWebSocketConnection = async (url: string): Promise => { - return new Promise((resolve) => { - const ws = new WebSocket(url); - - const timeout = setTimeout(() => { - ws.close(); - resolve(false); - }, 5000); - - ws.onopen = () => { - clearTimeout(timeout); - ws.close(); - resolve(true); - }; - - ws.onerror = () => { - clearTimeout(timeout); - resolve(false); - }; - }); -}; -``` - -This documentation provides a complete foundation for integrating real APIs and WebSocket connections into your Domain Development Navigator application using your established Event and Command contracts. diff --git a/devex-ui/index.html b/devex-ui/index.html index 3a9fbad1..beef4740 100644 --- a/devex-ui/index.html +++ b/devex-ui/index.html @@ -8,7 +8,7 @@ -
- +
+ diff --git a/devex-ui/package-lock.json b/devex-ui/package-lock.json index b4b0ca55..0d8987f5 100644 --- a/devex-ui/package-lock.json +++ b/devex-ui/package-lock.json @@ -45,6 +45,7 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "express": "^4.18.2", "input-otp": "^1.2.4", "json-schema-faker": "^0.5.9", "lucide-react": "^0.462.0", @@ -53,19 +54,23 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", + "remark-gfm": "^4.0.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "vaul": "^0.9.3", + "vite-plugin-markdown": "^2.2.0", "zod": "^3.23.8" }, "devDependencies": { + "@emotion/react": "^11.14.0", "@eslint/js": "^9.9.0", - "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/typography": "^0.5.16", "@types/node": "^22.5.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", @@ -96,10 +101,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -107,9 +158,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -117,13 +168,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", - "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -144,15 +195,59 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", - "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -189,6 +284,131 @@ "tough-cookie": "^4.1.4" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@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.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dev": true, + "license": "MIT", + "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" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@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", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -196,7 +416,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -213,7 +432,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -230,7 +448,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -247,7 +464,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -264,7 +480,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -281,7 +496,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -298,7 +512,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -315,7 +528,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -332,7 +544,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -349,7 +560,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -366,7 +576,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -383,7 +592,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -400,7 +608,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -417,7 +624,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -434,7 +640,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -451,7 +656,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -468,7 +672,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -502,7 +705,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -536,7 +738,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -553,7 +754,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -570,7 +770,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -587,7 +786,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -604,7 +802,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2607,7 +2804,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2621,7 +2817,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2635,7 +2830,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2649,7 +2843,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2663,7 +2856,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2677,7 +2869,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2691,7 +2882,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2705,7 +2895,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2719,7 +2908,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2733,7 +2921,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2747,7 +2934,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2761,7 +2947,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2775,7 +2960,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2789,7 +2973,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2803,7 +2986,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2817,7 +2999,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3051,10 +3232,11 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", - "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", @@ -3062,7 +3244,7 @@ "postcss-selector-parser": "6.0.10" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { @@ -3174,13 +3356,39 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3188,28 +3396,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.7.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3240,6 +3468,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", @@ -3469,6 +3703,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.1.tgz", @@ -3482,6 +3722,19 @@ "vite": "^4 || ^5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", @@ -3623,7 +3876,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -3638,6 +3890,12 @@ "node": ">=10" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -3676,6 +3934,32 @@ "postcss": "^8.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3694,6 +3978,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3750,6 +4073,44 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", @@ -3796,6 +4157,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3813,9 +4184,49 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { @@ -4353,6 +4764,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4369,6 +4790,34 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -4379,6 +4828,39 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4545,7 +5027,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4565,6 +5046,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4572,12 +5066,53 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4600,12 +5135,87 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.45", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", @@ -4647,11 +5257,68 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -4696,6 +5363,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4914,6 +5587,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4934,12 +5617,112 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "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" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5047,6 +5830,55 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5107,6 +5939,15 @@ "integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==", "license": "MIT" }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -5121,8 +5962,48 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fsevents": { - "version": "2.3.3", + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1" + } + }, + "node_modules/front-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/front-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, @@ -5154,6 +6035,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5163,6 +6068,19 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -5232,6 +6150,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5259,6 +6189,18 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -5271,6 +6213,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -5278,6 +6260,89 @@ "dev": true, "license": "MIT" }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5315,6 +6380,18 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, "node_modules/input-otp": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz", @@ -5343,6 +6420,46 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5370,6 +6487,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5400,6 +6527,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -5416,6 +6553,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5474,6 +6623,19 @@ "node": ">= 10.16.0" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5481,6 +6643,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-faker": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.9.tgz", @@ -5601,6 +6770,15 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5642,6 +6820,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6128,55 +7316,1009 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - } - }, + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -6270,6 +8412,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", @@ -6324,6 +8475,30 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ono": { "version": "4.0.11", "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz", @@ -6409,6 +8584,59 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6457,6 +8685,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6663,6 +8901,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -6686,6 +8947,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -6713,6 +8989,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -6774,6 +9074,33 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-remove-scroll": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", @@ -6976,6 +9303,72 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7043,7 +9436,6 @@ "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -7098,6 +9490,32 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -7120,6 +9538,84 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7141,6 +9637,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7163,6 +9731,16 @@ "react-dom": "^18.0.0" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7172,6 +9750,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7254,6 +9842,20 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -7304,6 +9906,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -7453,6 +10080,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -7469,6 +10105,26 @@ "node": ">=6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -7520,6 +10176,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -7558,13 +10227,106 @@ } } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -7575,6 +10337,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -7676,6 +10447,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -7689,6 +10469,15 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vaul": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", @@ -7702,6 +10491,34 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -7728,7 +10545,6 @@ "version": "5.4.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -7784,6 +10600,21 @@ } } }, + "node_modules/vite-plugin-markdown": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-markdown/-/vite-plugin-markdown-2.2.0.tgz", + "integrity": "sha512-eH2tXMZcx3EHb5okd+/0VIyoR8Gp9pGe24UXitOOcGkzObbJ1vl48aGOAbakoT88FBdzC8MXNkMfBIB9VK0Ndg==", + "license": "MIT", + "dependencies": { + "domhandler": "^4.0.0", + "front-matter": "^4.0.0", + "htmlparser2": "^6.0.0", + "markdown-it": "^12.0.0" + }, + "peerDependencies": { + "vite": ">= 2.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8027,6 +10858,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/devex-ui/package.json b/devex-ui/package.json index 9689e325..8520324b 100644 --- a/devex-ui/package.json +++ b/devex-ui/package.json @@ -7,8 +7,14 @@ "dev": "vite", "dev:mock": "vite --mode mock", "dev:real": "vite --mode real", + "dev:ssr": "node server.js", "build": "vite build", "build:dev": "vite build --mode development", + "build:ssr": "npm run build:ssr:client && npm run build:ssr:server", + "build:ssr:client": "SSR=false vite build --outDir dist/client", + "build:ssr:server": "SSR=true vite build --outDir dist/server", + "start": "NODE_ENV=production node server.js", + "vercel-build": "npm run build:ssr", "lint": "eslint .", "preview": "vite preview", "type-check": "tsc --noEmit" @@ -51,6 +57,7 @@ "cmdk": "^1.0.0", "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", + "express": "^4.18.2", "input-otp": "^1.2.4", "json-schema-faker": "^0.5.9", "lucide-react": "^0.462.0", @@ -59,19 +66,23 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", + "remark-gfm": "^4.0.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", "vaul": "^0.9.3", + "vite-plugin-markdown": "^2.2.0", "zod": "^3.23.8" }, "devDependencies": { + "@emotion/react": "^11.14.0", "@eslint/js": "^9.9.0", - "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/typography": "^0.5.16", "@types/node": "^22.5.5", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/devex-ui/public/logo.svg b/devex-ui/public/logo.svg index d281e36e..0b2f3956 100644 --- a/devex-ui/public/logo.svg +++ b/devex-ui/public/logo.svg @@ -1,27 +1,27 @@ - - + diff --git a/devex-ui/server.js b/devex-ui/server.js new file mode 100644 index 00000000..1622736a --- /dev/null +++ b/devex-ui/server.js @@ -0,0 +1,106 @@ +// devex-ui/server.js +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import express from 'express'; +import { createServer as createViteServer } from 'vite'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const isProduction = process.env.NODE_ENV === 'production'; + +async function createServer() { + const app = express(); + + let vite; + if (!isProduction) { + // Create Vite server in middleware mode + vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'custom' + }); + app.use(vite.middlewares); + } else { + // In production, serve the static files + app.use(express.static(path.resolve(__dirname, 'dist/client'))); + } + + app.use('*', async (req, res, next) => { + const url = req.originalUrl; + + // Check if this route should be server-side rendered + const isDocsRoute = url.startsWith('/docs'); + const isWelcomeRoute = url === '/welcome'; + const defaultPage = process.env.VITE_DEFAULT_PAGE || 'devx'; + const shouldApplySSR = isDocsRoute || (isWelcomeRoute && defaultPage === 'welcome'); + + if (!shouldApplySSR) { + // For routes that don't need SSR, just serve the index.html + if (isProduction) { + const indexHtml = fs.readFileSync( + path.resolve(__dirname, 'dist/client/index.html'), + 'utf-8' + ); + return res.status(200).set({ 'Content-Type': 'text/html' }).end(indexHtml); + } else { + // In development, let Vite transform the index.html + let indexHtml = fs.readFileSync( + path.resolve(__dirname, 'index.html'), + 'utf-8' + ); + indexHtml = await vite.transformIndexHtml(url, indexHtml); + return res.status(200).set({ 'Content-Type': 'text/html' }).end(indexHtml); + } + } + + try { + let template; + let render; + + if (!isProduction) { + // In development, get the template from index.html and transform it with Vite + template = fs.readFileSync( + path.resolve(__dirname, 'index.html'), + 'utf-8' + ); + template = await vite.transformIndexHtml(url, template); + + // Load the server entry module + const { render: ssrRender } = await vite.ssrLoadModule('/src/entry-server.tsx'); + render = ssrRender; + } else { + // In production, use the built files + template = fs.readFileSync( + path.resolve(__dirname, 'dist/client/index.html'), + 'utf-8' + ); + const { render: ssrRender } = await import('./dist/server/entry-server.js'); + render = ssrRender; + } + + // Render the app + const context = {}; + const { html: appHtml } = await render(url, context); + + if (context.url) { + return res.redirect(301, context.url); + } + + const html = template.replace('', appHtml); + + res.status(200).set({ 'Content-Type': 'text/html' }).end(html); + } catch (e) { + if (!isProduction) { + vite.ssrFixStacktrace(e); + } + console.error(e); + res.status(500).end(e.message); + } + }); + + const port = process.env.PORT || 8081; + app.listen(port, () => { + console.log(`Server started at http://localhost:${port}`); + }); +} + +createServer(); \ No newline at end of file diff --git a/devex-ui/src/App.tsx b/devex-ui/src/App.tsx index 3d110114..146a0e62 100644 --- a/devex-ui/src/App.tsx +++ b/devex-ui/src/App.tsx @@ -1,12 +1,14 @@ // devex-ui/src/App.tsx -import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { Routes, Route } from 'react-router-dom'; import { TooltipProvider } from '@/components/ui/tooltip'; import ErrorBoundary from '@/components/ErrorBoundary'; import { AppProvider } from '@/app/AppProvider'; import Index from './pages/Index'; +import DocsPage from './pages/DocsPage'; import NotFound from './pages/NotFound'; - +import { Navigate } from 'react-router-dom'; +import WelcomePage from "@/pages/WelcomePage.tsx"; /** all sidebar slugs except the default “dashboard” */ const VIEWS = [ 'commands', @@ -21,26 +23,32 @@ const VIEWS = [ ] as const; const App = () => ( - - - - - - {/* dashboard */} - } /> + + + + + {/* Docs: root + /docs */} + + } + /> + } /> + } /> + } /> - {/* other sidebar views → same Index page for now */} - {VIEWS.map(view => ( - } /> - ))} + {/* DevX UI */} + } /> + {VIEWS.map(view => ( + } /> + ))} - {/* fallback */} - } /> - - - - - + } /> + + + + ); export default App; diff --git a/devex-ui/src/app/AppProvider.tsx b/devex-ui/src/app/AppProvider.tsx index 1641a51e..8482c13b 100644 --- a/devex-ui/src/app/AppProvider.tsx +++ b/devex-ui/src/app/AppProvider.tsx @@ -1,3 +1,4 @@ +//devex-ui/src/app/AppProvider.tsx import { createContext, useContext, useState, ReactNode, useEffect, useSyncExternalStore } from 'react'; @@ -5,6 +6,24 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from '@/data/queryClient'; import { Toaster, toast } from '@/components/ui/sonner'; +// Check if we're in a browser environment +const isBrowser = typeof window !== 'undefined'; + +// Safely access localStorage +const getLocalStorage = (key: string): string | null => { + if (isBrowser) { + return localStorage.getItem(key); + } + return null; +}; + +// Safely set localStorage +const setLocalStorage = (key: string, value: string): void => { + if (isBrowser) { + localStorage.setItem(key, value); + } +}; + type Flags = Record; interface Ctx { @@ -19,9 +38,15 @@ export const useAppCtx = () => useContext(Ctx)!; const useLocalSetting = (key:string, fallback:T)=>{ return useSyncExternalStore( - cb => { window.addEventListener('storage',cb); return ()=>window.removeEventListener('storage',cb); }, + cb => { + if (isBrowser) { + window.addEventListener('storage',cb); + return () => window.removeEventListener('storage',cb); + } + return () => {}; + }, () => { - const value = localStorage.getItem(key); + const value = getLocalStorage(key); if (value === null) return fallback; try { return JSON.parse(value); @@ -37,21 +62,28 @@ const useLocalSetting = (key:string, fallback:T)=>{ export function AppProvider({ children }: { children: ReactNode }) { const tenant = useLocalSetting('tenant','tenant-1'); const role = useLocalSetting('role','admin'); - const [flags, setFlags] = useState( - JSON.parse(localStorage.getItem('feature_flags') || '{}'), - ); + const [flags, setFlags] = useState(() => { + const storedFlags = getLocalStorage('feature_flags'); + return storedFlags ? JSON.parse(storedFlags) : {}; + }); const setTenant = (t:string)=>{ - localStorage.setItem('tenant',t); - window.dispatchEvent(new Event('storage')); // force same-tab update + setLocalStorage('tenant',t); + if (isBrowser) { + window.dispatchEvent(new Event('storage')); // force same-tab update + } }; const setRole = (r:string)=>{ - localStorage.setItem('role',r); - window.dispatchEvent(new Event('storage')); + setLocalStorage('role',r); + if (isBrowser) { + window.dispatchEvent(new Event('storage')); + } }; // Listen for storage events from other tabs useEffect(() => { + if (!isBrowser) return; + const handleStorageChange = (e: StorageEvent) => { if (e.key === 'feature_flags' && e.newValue) { setFlags(JSON.parse(e.newValue)); @@ -65,8 +97,10 @@ export function AppProvider({ children }: { children: ReactNode }) { const toggleFlag = (id: string, v: boolean = !flags[id]) => { const next = { ...flags, [id]: v }; setFlags(next); - localStorage.setItem('feature_flags', JSON.stringify(next)); - window.dispatchEvent(new Event('featureFlagsUpdated')); // cross-tab sync + setLocalStorage('feature_flags', JSON.stringify(next)); + if (isBrowser) { + window.dispatchEvent(new Event('featureFlagsUpdated')); // cross-tab sync + } }; return ( diff --git a/devex-ui/src/components/DocsFooter.tsx b/devex-ui/src/components/DocsFooter.tsx new file mode 100644 index 00000000..f7a2536a --- /dev/null +++ b/devex-ui/src/components/DocsFooter.tsx @@ -0,0 +1,14 @@ +//devex-ui/src/components/DocsFooter.tsx +import { Copyright } from 'lucide-react'; + +export const DocsFooter = () => { + return ( +
+
+ + 2025 DevHeart Technologies Inc +
+
made with <3
+
+ ); +}; diff --git a/devex-ui/src/components/DocsHeader.tsx b/devex-ui/src/components/DocsHeader.tsx new file mode 100644 index 00000000..47b8662d --- /dev/null +++ b/devex-ui/src/components/DocsHeader.tsx @@ -0,0 +1,41 @@ +//devex-ui/src/components/DocsHeader.tsx +import { Logo } from './Logo'; +import { Link } from 'react-router-dom'; +import { Github } from 'lucide-react'; + +interface DocsHeaderProps { + section?: string; +} + +export const DocsHeader = ({ section }: DocsHeaderProps) => { + return ( +
+ {/* Left: Logo + Title */} +
+ + + Intent + + {section && ( + | {section} + )} +
+ + {/* Right: GitHub link */} + +
+ ); +}; diff --git a/devex-ui/src/components/DocsSidebar.tsx b/devex-ui/src/components/DocsSidebar.tsx new file mode 100644 index 00000000..2d44f782 --- /dev/null +++ b/devex-ui/src/components/DocsSidebar.tsx @@ -0,0 +1,180 @@ +// devex-ui/src/components/DocsSidebar.tsx +import {useState} from 'react'; +import {useLocation, useNavigate} from 'react-router-dom'; +import { + Book, + FileText, + Code, + BookOpen, + ExternalLink, + ChevronLeft, + ChevronRight, + ChevronDown, + ChevronUp, + Home, LayoutDashboard, +} from 'lucide-react'; +import {cn} from '@/lib/utils'; +import {Button} from '@/components/ui/button'; + +/* ─────────────────────── Types / constants ────────────────────── */ + +type View = string; + +interface DocsSidebarProps { + onViewChange?: (view: View) => void; + activeView?: View; +} + +interface NavItem { + id: View; + label: string; + icon: React.ElementType; + children?: { id: string; label: string }[]; +} + +const NAV_ITEMS: NavItem[] = [ + { + id: 'basics/introduction', + label: 'Basics', + icon: FileText, + children: [ + {id: 'basics/introduction', label: 'Introduction'}, + {id: 'basics/project-structure', label: 'Project Structure'}, + {id: 'basics/quickstart', label: 'Quickstart'}, + ], + }, + { + id: 'architecture/architecture-overview', + label: 'Architecture', + icon: Book, + children: [ + {id: 'architecture/architecture-overview', label: 'Overview'}, + {id: 'architecture/cqrs-projections', label: 'CQRS & Projections'}, + {id: 'architecture/domain-modeling', label: 'Domain Modeling'}, + {id: 'architecture/temporal-workflows', label: 'Temporal Workflows'}, + {id: 'architecture/multi-tenancy-details', label: 'Multi-Tenancy'}, + {id: 'architecture/observability-details', label: 'Observability'}, + {id: 'architecture/testing-strategies', label: 'Testing Strategies'}, + ], + }, + { + id: 'devx/devx-ui', + label: 'DevX', + icon: Code, + children: [ + {id: 'devx/devx-ui', label: 'DevX UI'}, + {id: 'devx/cli-tools', label: 'CLI Tools'}, + ], + }, + { + id: 'reflections/index', + label: 'Reflections', + icon: BookOpen, + children: [ + {id: 'reflections/index', label: 'Overview'}, + {id: 'reflections/note-cqrs-projections', label: 'CQRS & Projections'}, + {id: 'reflections/note-domain-modeling', label: 'Domain Modeling'}, + {id: 'reflections/note-event-sourcing', label: 'Event Sourcing'}, + {id: 'reflections/note-multi-tenancy', label: 'Multi-Tenancy'}, + {id: 'reflections/note-observability', label: 'Observability'}, + {id: 'reflections/note-temporal-workflows', label: 'Temporal Workflows'}, + {id: 'reflections/note-testing-strategies', label: 'Testing Strategies'}, + ], + }, +]; + +const viewFromPath = (path: string): View => { + const match = path.match(/^\/docs\/?(.*)$/); + const slug = match?.[1] || 'basics/introduction'; + return slug as View; +}; + +/* ───────────────────────── component ───────────────────────────── */ + +export const DocsSidebar = ({onViewChange, activeView}: DocsSidebarProps) => { + const navigate = useNavigate(); + const {pathname} = useLocation(); + const [expandedItems, setExpandedItems] = useState>({ + guidelines: true, // Start with guidelines expanded + }); + + /* source of truth: prefer explicit prop, else derive from URL */ + const current = viewFromPath(pathname); + + const toggleExpand = (id: string) => { + setExpandedItems(prev => ({ + ...prev, + [id]: !prev[id] + })); + }; + + return ( + + ); +}; diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index f70a5506..bd7721cf 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -54,14 +54,12 @@ export const Header = () => { {/* Spacer */}
- {/* System Status Indicators */} + {/* API type indicators */} +
-
-
+ {!isMock &&
} + {isMock &&
}
- - {apiMode.toUpperCase()} API - ); }; diff --git a/devex-ui/src/components/Logo.tsx b/devex-ui/src/components/Logo.tsx index a69490dd..e5beaac1 100644 --- a/devex-ui/src/components/Logo.tsx +++ b/devex-ui/src/components/Logo.tsx @@ -1,3 +1,4 @@ +//devex-ui/src/components/Logo.tsx export const Logo = () => ( (view === 'dashboard' ? '/' : `/${view}`); +const pathForView = (view: View) => (view === 'dashboard' ? '/devx' : `/devx/${view}`); const viewFromPath = (path: string): View => { - const slug = path === '/' ? 'dashboard' : path.replace(/^\//, ''); + const match = path.match(/^\/devx\/?([^\/]*)/); + const slug = match?.[1] || 'dashboard'; return slug as View; }; -/* ───────────────────────── component ───────────────────────────── */ - export const Sidebar = ({ onViewChange, activeView }: SidebarProps) => { const [isCollapsed, setIsCollapsed] = useState(false); const { enabled } = useFeatures(); @@ -87,7 +84,7 @@ export const Sidebar = ({ onViewChange, activeView }: SidebarProps) => { return ( ); diff --git a/devex-ui/src/config/apiMode.ts b/devex-ui/src/config/apiMode.ts index be9f09c9..66eb7234 100644 --- a/devex-ui/src/config/apiMode.ts +++ b/devex-ui/src/config/apiMode.ts @@ -1,3 +1,4 @@ +//devex-ui/src/config/apiMode.ts // Determine API mode with cascade: // 1. URL param: ?apiMode=mock|real // 2. localStorage: api_mode @@ -7,20 +8,30 @@ // Default from environment or fallback to 'mock' const envDefault = import.meta.env.VITE_API_MODE || 'mock'; -// Check URL param -const urlParams = new URLSearchParams(window.location.search); -const urlMode = urlParams.get('apiMode'); +// Check if we're in a browser environment +const isBrowser = typeof window !== 'undefined'; -// Check localStorage -const storageMode = localStorage.getItem('api_mode'); +// Variables to store URL and localStorage values +let urlMode = null; +let storageMode = null; + +// Only access browser-specific APIs if in browser environment +if (isBrowser) { + // Check URL param + const urlParams = new URLSearchParams(window.location.search); + urlMode = urlParams.get('apiMode'); + + // Check localStorage + storageMode = localStorage.getItem('api_mode'); + + // If URL param is set, update localStorage for persistence + if (urlMode) { + localStorage.setItem('api_mode', urlMode); + } +} // Determine mode with cascade export const apiMode = urlMode || storageMode || envDefault; // Convenience boolean for conditionals export const isMock = apiMode === 'mock'; - -// If URL param is set, update localStorage for persistence -if (urlMode) { - localStorage.setItem('api_mode', urlMode); -} \ No newline at end of file diff --git a/devex-ui/src/data/api.ts b/devex-ui/src/data/api.ts index a4f0c2a2..27021891 100644 --- a/devex-ui/src/data/api.ts +++ b/devex-ui/src/data/api.ts @@ -1,9 +1,20 @@ //devex-ui/src/data/api.ts -const apiMode = localStorage.getItem('api_mode') || import.meta.env.VITE_API_MODE || 'mock'; +// Check if we're in a browser environment +const isBrowser = typeof window !== 'undefined'; + +// Safely access localStorage +const getLocalStorage = (key: string): string | null => { + if (isBrowser) { + return localStorage.getItem(key); + } + return null; +}; + +const apiMode = getLocalStorage('api_mode') || import.meta.env.VITE_API_MODE || 'mock'; // API client configuration export const API_CONFIG = { - baseUrl: localStorage.getItem('api_uri') || import.meta.env.VITE_API_URL || '', + baseUrl: getLocalStorage('api_uri') || import.meta.env.VITE_API_URL || '', wsUrl: import.meta.env.VITE_WS_URL || 'ws://localhost:8080/events/stream', endpoints: { events: '/api/events', @@ -17,8 +28,13 @@ export const API_CONFIG = { // --- URL builder function buildUrl(endpoint: string, params?: Record): string { - const base = apiMode === 'mock' ? '' : (localStorage.getItem('api_uri') || import.meta.env.VITE_API_URL || ''); - const url = new URL(`${base}${endpoint}`, window.location.origin); + const base = apiMode === 'mock' ? '' : (getLocalStorage('api_uri') || import.meta.env.VITE_API_URL || ''); + + // Use a default origin for SSR + const origin = isBrowser ? window.location.origin : 'http://localhost'; + + // Create URL with the appropriate origin + const url = new URL(`${base}${endpoint}`, origin); if (params) { Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v)); diff --git a/devex-ui/src/entry-client.tsx b/devex-ui/src/entry-client.tsx new file mode 100644 index 00000000..e01a61dd --- /dev/null +++ b/devex-ui/src/entry-client.tsx @@ -0,0 +1,39 @@ +// devex-ui/src/entry-client.tsx +import React from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; +import { setupMocks } from './setupMocks'; + +// Wait for mocks to be set up before hydrating +setupMocks().then(() => { + const root = document.getElementById('root'); + + if (root) { + // Check if the current route should be server-side rendered + const isDocsRoute = window.location.pathname.startsWith('/docs'); + const isWelcomeRoute = window.location.pathname === '/welcome'; + const defaultPage = import.meta.env.VITE_DEFAULT_PAGE || 'devx'; + const wasServerRendered = isDocsRoute || (isWelcomeRoute && defaultPage === 'welcome'); + + if (wasServerRendered) { + // Hydrate the server-rendered HTML + hydrateRoot( + root, + + + + ); + } else { + // For routes that weren't server-rendered, use createRoot + import('react-dom/client').then(({ createRoot }) => { + createRoot(root).render( + + + + ); + }); + } + } +}); \ No newline at end of file diff --git a/devex-ui/src/entry-server.tsx b/devex-ui/src/entry-server.tsx new file mode 100644 index 00000000..020e4624 --- /dev/null +++ b/devex-ui/src/entry-server.tsx @@ -0,0 +1,27 @@ +// devex-ui/src/entry-server.tsx +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import { StaticRouter } from 'react-router-dom/server'; +import App from './App'; + +export function render(url: string, context: any) { + // Only apply SSR to docs/**/* routes and WelcomePage when VITE_DEFAULT_PAGE === 'welcome' + const isDocsRoute = url.startsWith('/docs'); + const isWelcomeRoute = url === '/welcome'; + const defaultPage = process.env.VITE_DEFAULT_PAGE || 'devx'; + const shouldApplySSR = isDocsRoute || (isWelcomeRoute && defaultPage === 'welcome'); + + if (!shouldApplySSR) { + // Return empty HTML for routes that don't need SSR + return { html: '' }; + } + + // Render the app to a string + const html = renderToString( + + + + ); + + return { html }; +} \ No newline at end of file diff --git a/devex-ui/src/graph/edgeUtils.ts b/devex-ui/src/graph/edgeUtils.ts index 8ebd7f3c..183219a0 100644 --- a/devex-ui/src/graph/edgeUtils.ts +++ b/devex-ui/src/graph/edgeUtils.ts @@ -1,3 +1,4 @@ +//devex-ui/src/graph/edgeUtils.ts export interface Edge { from:string; to:string; type:'causation' } export function generateEdges( diff --git a/devex-ui/src/hooks/useFeatures.ts b/devex-ui/src/hooks/useFeatures.ts index 587326bf..d28b7577 100644 --- a/devex-ui/src/hooks/useFeatures.ts +++ b/devex-ui/src/hooks/useFeatures.ts @@ -1,3 +1,4 @@ +//devex-ui/src/hooks/useFeatures.ts import { useAppCtx } from '@/app/AppProvider'; export function useFeatures() { const { flags, toggleFlag } = useAppCtx(); diff --git a/devex-ui/src/hooks/ws/useEventStream.ts b/devex-ui/src/hooks/ws/useEventStream.ts index 7603f520..1e3ca957 100644 --- a/devex-ui/src/hooks/ws/useEventStream.ts +++ b/devex-ui/src/hooks/ws/useEventStream.ts @@ -1,3 +1,4 @@ +//devex-ui/src/hooks/ws/useEventStream.ts import { useEffect } from 'react'; import { isMock } from '@/config/apiMode'; import { createEventStream } from '@/mocks/stores/event.store'; diff --git a/devex-ui/src/mocks/factories/command.factory.ts b/devex-ui/src/mocks/factories/command.factory.ts index 60cc65d3..96a6de30 100644 --- a/devex-ui/src/mocks/factories/command.factory.ts +++ b/devex-ui/src/mocks/factories/command.factory.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/factories/command.factory.ts import { v4 as uuid } from 'uuid'; import type { Command, Metadata } from '@/data/types'; diff --git a/devex-ui/src/mocks/factories/event.factory.ts b/devex-ui/src/mocks/factories/event.factory.ts index f87cc1f8..42444cd5 100644 --- a/devex-ui/src/mocks/factories/event.factory.ts +++ b/devex-ui/src/mocks/factories/event.factory.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/factories/event.factory.ts import { v4 as uuid } from 'uuid'; import type { Event, Metadata } from '@/data/types'; diff --git a/devex-ui/src/mocks/factories/log.factory.ts b/devex-ui/src/mocks/factories/log.factory.ts index 7f49c9ed..1d394f45 100644 --- a/devex-ui/src/mocks/factories/log.factory.ts +++ b/devex-ui/src/mocks/factories/log.factory.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/factories/log.factory.ts import { v4 as uuid } from 'uuid'; export interface LogLine { diff --git a/devex-ui/src/mocks/scenarios/default.ts b/devex-ui/src/mocks/scenarios/default.ts index 47776feb..70b7b2b1 100644 --- a/devex-ui/src/mocks/scenarios/default.ts +++ b/devex-ui/src/mocks/scenarios/default.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/scenarios/default.ts //src/devex-ui/src/mocks/scenarios/default.ts import { eventStore, diff --git a/devex-ui/src/mocks/stores/command.store.ts b/devex-ui/src/mocks/stores/command.store.ts index 14c02edf..a6987e7c 100644 --- a/devex-ui/src/mocks/stores/command.store.ts +++ b/devex-ui/src/mocks/stores/command.store.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/command.store.ts import { createStore } from './createStore'; import { makeCommand } from '../factories/command.factory'; import type { Command } from '@/data/types'; diff --git a/devex-ui/src/mocks/stores/createStore.ts b/devex-ui/src/mocks/stores/createStore.ts index 7e82c2dd..c5335a80 100644 --- a/devex-ui/src/mocks/stores/createStore.ts +++ b/devex-ui/src/mocks/stores/createStore.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/createStore.ts /** dev-only in-mem store */ interface Identifiable { id:string } diff --git a/devex-ui/src/mocks/stores/event.store.ts b/devex-ui/src/mocks/stores/event.store.ts index 66db5c1b..4952c039 100644 --- a/devex-ui/src/mocks/stores/event.store.ts +++ b/devex-ui/src/mocks/stores/event.store.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/event.store.ts import { createStore } from './createStore'; import { makeEvent } from '../factories/event.factory'; import type { Event } from '@/data/types'; diff --git a/devex-ui/src/mocks/stores/index.ts b/devex-ui/src/mocks/stores/index.ts index d3391e1e..03cac03b 100644 --- a/devex-ui/src/mocks/stores/index.ts +++ b/devex-ui/src/mocks/stores/index.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/index.ts // keep alphabetic for roll-up tree-shaking export * from './command.store' export * from './event.store' diff --git a/devex-ui/src/mocks/stores/log.store.ts b/devex-ui/src/mocks/stores/log.store.ts index bfb011d9..528bf451 100644 --- a/devex-ui/src/mocks/stores/log.store.ts +++ b/devex-ui/src/mocks/stores/log.store.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/log.store.ts import { createStore } from './createStore'; import { makeLog, type LogLine } from '../factories/log.factory'; diff --git a/devex-ui/src/mocks/stores/roles.store.ts b/devex-ui/src/mocks/stores/roles.store.ts index 20331f06..00c5ac14 100644 --- a/devex-ui/src/mocks/stores/roles.store.ts +++ b/devex-ui/src/mocks/stores/roles.store.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/roles.store.ts import { createStore } from './createStore'; // Define the role entry type with domain as id to satisfy Identifiable diff --git a/devex-ui/src/mocks/stores/trace.store.ts b/devex-ui/src/mocks/stores/trace.store.ts index b7550112..a1e15278 100644 --- a/devex-ui/src/mocks/stores/trace.store.ts +++ b/devex-ui/src/mocks/stores/trace.store.ts @@ -1,3 +1,4 @@ +//devex-ui/src/mocks/stores/trace.store.ts import { createStore } from './createStore'; import { makeTrace } from '../factories/trace.factory'; import { generateEdges } from '@/graph/edgeUtils'; diff --git a/devex-ui/src/pages/DocsPage.tsx b/devex-ui/src/pages/DocsPage.tsx new file mode 100644 index 00000000..e430beb4 --- /dev/null +++ b/devex-ui/src/pages/DocsPage.tsx @@ -0,0 +1,111 @@ +// devex-ui/src/pages/DocsPage.tsx +import { useLocation } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { DocsHeader } from '@/components/DocsHeader'; +import { DocsSidebar } from '@/components/DocsSidebar'; + +import welcome from '$docs/basics/introduction.md?raw'; +import projectStructure from '$docs/basics/project-structure.md?raw'; +import quickstart from '$docs/basics/quickstart.md?raw'; + +import archOverview from '$docs/architecture/architecture-overview.md?raw'; +import cqrs from '$docs/architecture/cqrs-projections.md?raw'; +import domain from '$docs/architecture/domain-modeling.md?raw'; +import temporal from '$docs/architecture/temporal-workflows.md?raw'; +import tenancy from '$docs/architecture/multi-tenancy-details.md?raw'; +import observability from '$docs/architecture/observability-details.md?raw'; +import testing from '$docs/architecture/testing-strategies.md?raw'; + +import devxUi from '$docs/devx/devx-ui.md?raw'; +import cli from '$docs/devx/cli-tools.md?raw'; + +import reflections from '$docs/reflections/index.md?raw'; +import noteCQRS from '$docs/reflections/note-cqrs-projections.md?raw'; +import noteDomain from '$docs/reflections/note-domain-modeling.md?raw'; +import noteES from '$docs/reflections/note-event-sourcing.md?raw'; +import noteTenancy from '$docs/reflections/note-multi-tenancy.md?raw'; +import noteObs from '$docs/reflections/note-observability.md?raw'; +import noteTemporal from '$docs/reflections/note-temporal-workflows.md?raw'; +import noteTesting from '$docs/reflections/note-testing-strategies.md?raw'; +import {DocsFooter} from "@/components/DocsFooter.tsx"; + +const docsMap: Record = { + 'basics/introduction': welcome, + 'basics/project-structure': projectStructure, + 'basics/quickstart': quickstart, + 'architecture/architecture-overview': archOverview, + 'architecture/cqrs-projections': cqrs, + 'architecture/domain-modeling': domain, + 'architecture/temporal-workflows': temporal, + 'architecture/multi-tenancy-details': tenancy, + 'architecture/observability-details': observability, + 'architecture/testing-strategies': testing, + 'devx/devx-ui': devxUi, + 'devx/cli-tools': cli, + 'reflections/index': reflections, + 'reflections/note-cqrs-projections': noteCQRS, + 'reflections/note-domain-modeling': noteDomain, + 'reflections/note-event-sourcing': noteES, + 'reflections/note-multi-tenancy': noteTenancy, + 'reflections/note-observability': noteObs, + 'reflections/note-temporal-workflows': noteTemporal, + 'reflections/note-testing-strategies': noteTesting, +}; + +export default function DocsPage() { + const location = useLocation(); + const slug = location.pathname.replace(/^\/docs\//, '') || 'basics/introduction'; + const [content, setContent] = useState(null); + + useEffect(() => { + if (!docsMap[slug]) { + setContent(`# 404\nPage \`${slug}\` not found.`); + } else { + setContent(docsMap[slug]); + } + }, [slug]); + + return ( +
+ +
+ +
+ { + const match = href.match(/^([^#]+)\.md(#.*)?$/); + const link = match?.[1]; + const fragment = match?.[2] || ''; + + if (!link) { + return ( + + {children} + + ); + } + + // Base: current doc's path minus filename + const basePath = slug.split('/').slice(0, -1).join('/'); // e.g. architecture + const resolvedPath = `${basePath}/${link}`.replace(/\/+/, '/'); + + return ( + + {children} + + ); + } + }} + > + {content || 'Loading...'} + +
+
+ +
+ ); +} diff --git a/devex-ui/src/pages/Index.tsx b/devex-ui/src/pages/Index.tsx index 557f659a..a9d85833 100644 --- a/devex-ui/src/pages/Index.tsx +++ b/devex-ui/src/pages/Index.tsx @@ -1,6 +1,7 @@ //devex-ui/src/pages/Index.tsx import { useState } from "react"; +import { useLocation } from "react-router-dom"; import { Header } from "@/components/Header"; import { Sidebar } from "@/components/Sidebar"; import { Dashboard } from "@/components/Dashboard"; @@ -20,8 +21,13 @@ import { useAppCtx } from '@/app/AppProvider'; type ActiveView = 'dashboard' | 'commands' | 'events' | 'projections' | 'traces' | 'aggregates' | 'status' | 'rewind' | 'ai' | 'settings'; const Index = () => { - const initialView = window.location.pathname.replace(/^\//, '') as ActiveView || 'dashboard'; - const [activeView, setActiveView] = useState(initialView); + const location = useLocation(); + const path = location.pathname; + const activeView = (() => { + const match = path.match(/^\/devx\/?([^\/]*)/); + const slug = match?.[1] || 'dashboard'; + return slug as ActiveView; + })(); const [isAICompanionOpen, setIsAICompanionOpen] = useState(false); const { tenant, role, flags } = useAppCtx(); @@ -74,9 +80,7 @@ const Index = () => { const handleViewChange = (view: string) => { if (view === 'ai') { setIsAICompanionOpen(true); - return; } - setActiveView(view as ActiveView); }; const shouldShowAICompanion = flags.ai === true; diff --git a/devex-ui/src/pages/WelcomePage.tsx b/devex-ui/src/pages/WelcomePage.tsx new file mode 100644 index 00000000..93994e32 --- /dev/null +++ b/devex-ui/src/pages/WelcomePage.tsx @@ -0,0 +1,92 @@ +// devex-ui/src/pages/WelcomePage.tsx +import { DocsHeader } from "@/components/DocsHeader"; +import { DocsSidebar } from "@/components/DocsSidebar"; +import { Card, CardContent } from "@/components/ui/card"; +import { DocsFooter } from '@/components/DocsFooter'; + +const WelcomePage = () => { + return ( +
+ + +
+ + +
+

Welcome

+

+ Intent turns event-sourcing theory into a platform you can demo in five minutes. + It's a pragmatic, ports-first reference for multi-tenant, event-sourced CQRS back-ends + powered by TypeScript and uses Temporal for durable workflow execution. +

+ + + +

Highlights

+
+ + +

Lossless backend processing

+

+ Event-sourced core guarantees no data loss, even under retries, crashes, or partial failures. + Structure follows DDD. Every command, event, and projection is persisted and replayable. +

+
+
+ + +

Ports-first hexagon

+

+ Technology-agnostic core logic. Adapters for PostgreSQL (event store + RLS) and + Temporal (workflows) plug in via explicit, testable ports. +

+
+
+ + +

Tenant isolation by default

+

+ Tenant IDs propagate edge → core → infra. Row isolation in DB and namespaced + workflows prevent accidental cross-tenant access or leaks. +

+
+
+ + +

Production-grade observability

+

+ Unified structured logging with context-aware LoggerPort, customizable log levels, + and error serialization. OpenTelemetry spans wrap all key flows. +

+
+
+
+
+
+ +
+ ); +}; + +export default WelcomePage; diff --git a/devex-ui/tailwind.config.ts b/devex-ui/tailwind.config.ts index 8ff25367..b4a80567 100644 --- a/devex-ui/tailwind.config.ts +++ b/devex-ui/tailwind.config.ts @@ -1,5 +1,6 @@ import type { Config } from "tailwindcss"; import tailwindcssAnimate from "tailwindcss-animate"; +import typography from '@tailwindcss/typography'; export default { darkMode: ["class"], @@ -90,8 +91,22 @@ export default { animation: { 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out' - } + }, + typography: ({ theme }) => ({ + invert: { + css: { + color: theme('colors.slate.300'), + a: { color: theme('colors.blue.400'), '&:hover': { color: theme('colors.blue.500') } }, + h1: { color: theme('colors.white') }, + h2: { color: theme('colors.white') }, + h3: { color: theme('colors.white') }, + code: { color: theme('colors.pink.400') }, + 'blockquote p:first-of-type::before': { content: 'none' }, + 'blockquote p:first-of-type::after': { content: 'none' }, + } + }, + }), } }, - plugins: [tailwindcssAnimate], + plugins: [typography, tailwindcssAnimate], } satisfies Config; diff --git a/devex-ui/vercel.json b/devex-ui/vercel.json index 1db5d80e..974df929 100644 --- a/devex-ui/vercel.json +++ b/devex-ui/vercel.json @@ -1,5 +1,34 @@ { - "rewrites": [ - { "source": "/(.*)", "destination": "/" } + "version": 2, + "builds": [ + { + "src": "package.json", + "use": "@vercel/static-build", + "config": { + "distDir": "dist/client" + } + }, + { + "src": "api/ssr.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/assets/(.*)", + "dest": "/assets/$1" + }, + { + "src": "/_assets/(.*)", + "dest": "/_assets/$1" + }, + { + "src": "/(.*\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$)", + "dest": "/$1" + }, + { + "src": "/(.*)", + "dest": "/api/ssr.js" + } ] -} \ No newline at end of file +} diff --git a/devex-ui/vite.config.ts b/devex-ui/vite.config.ts index b99a0138..53b525b7 100644 --- a/devex-ui/vite.config.ts +++ b/devex-ui/vite.config.ts @@ -2,27 +2,54 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import path from "path"; import { componentTagger } from "lovable-tagger"; +import { plugin as Markdown } from 'vite-plugin-markdown'; // https://vitejs.dev/config/ -export default defineConfig(({ mode }) => ({ - server: { - host: "::", - port: 8080, - historyApiFallback: true - }, - plugins: [ - react(), - mode === 'development' && - componentTagger(), - ].filter(Boolean), - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), +export default defineConfig(({ mode, command }) => { + const isSSR = command === 'build' && process.env.SSR === 'true'; + + return { + root: './', + server: { + host: "::", + port: 8080, + historyApiFallback: true, + fs: { + allow: ['..'], // allow ../docs + }, + }, + plugins: [ + react({ + jsxImportSource: '@emotion/react', + plugins: [], + // Enable SSR + ssr: isSSR, + }), + mode === 'development' && + componentTagger(), + Markdown({ mode: ['raw'] }), + ].filter(Boolean), + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "$docs": path.resolve(__dirname, "./docs"), //symlink follow + }, + }, + define: { + 'process.env.VITE_API_MODE': JSON.stringify(process.env.VITE_API_MODE || 'mock'), + 'process.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL || ''), + 'process.env.VITE_API_NO_SWITCH': JSON.stringify(process.env.VITE_API_NO_SWITCH || ''), + 'import.meta.env.VITE_DEFAULT_PAGE': JSON.stringify(process.env.VITE_DEFAULT_PAGE || 'devx') + }, + // SSR configuration + build: { + ssr: isSSR ? './src/entry-server.tsx' : undefined, + outDir: isSSR ? 'dist/server' : 'dist/client', + rollupOptions: { + input: isSSR + ? './src/entry-server.tsx' + : './index.html', + }, }, - }, - define: { - 'process.env.VITE_API_MODE': JSON.stringify(process.env.VITE_API_MODE || 'mock'), - 'process.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL || ''), - 'process.env.VITE_API_NO_SWITCH': JSON.stringify(process.env.VITE_API_NO_SWITCH || '') - }, -})); + }; +}); diff --git a/docs/architecture/architecture-overview.md b/docs/architecture/architecture-overview.md new file mode 100644 index 00000000..fa98157e --- /dev/null +++ b/docs/architecture/architecture-overview.md @@ -0,0 +1,89 @@ +# System Architecture Overview + +Intent is designed as a multi-tenant, hexagonal backend that serves as a pragmatic, principled reference implementation for event-sourced CQRS systems. This document provides a high-level overview of Intent's architecture for experienced developers or those seeking the "big picture." + +## Key Architectural Patterns + +Intent combines several modern software design patterns and approaches: + +1. **Domain-Driven Design (DDD)**: The codebase is organized around the business domain, with clear separation between core domain logic and infrastructure concerns. Domain concepts are modeled as aggregates, commands, events, and sagas, reflecting real-world business processes. + +2. **Event Sourcing**: The system uses events as the source of truth, storing all changes to the application state as a sequence of events. This provides a complete audit trail and enables temporal queries (what was the state at a given point in time). + +3. **Command Query Responsibility Segregation (CQRS)**: Commands (write operations) and queries (read operations) are separated, with different models for writing and reading data. This allows for optimized read models and scalability. + +4. **Temporal Workflow Orchestration**: Complex business processes are orchestrated using Temporal, providing durability and reliability for long-running operations. Workflows can be paused, resumed, and retried automatically. + +5. **PostgreSQL Event Store with Snapshots**: Events are stored in PostgreSQL, with support for snapshots to optimize performance when loading aggregates with long histories. + +6. **Read Model Projections**: Events are projected into read-optimized models to support efficient querying. Projections are defined in code and automatically kept in sync with the database schema. + +7. **Multi-tenancy**: The system is designed to support multiple tenants, with tenant isolation at various levels (database, domain, API, processing). Every command, event, and database table includes tenant information. + +8. **Observability**: The system includes comprehensive tracing and monitoring capabilities for operational visibility, using OpenTelemetry for distributed tracing and structured logging. + +## Architecture Layers + +Intent follows a hexagonal (ports-and-adapters) architecture, which is reflected in its directory structure: + +### 1. Core (Domain Layer) - `src/core/` + +- Contains pure business logic: aggregates, commands, events, and sagas +- No dependency on infrastructure; replay-safe and testable +- Organized into domain-specific vertical slices (e.g., `system/`, `orders/`) + +The Core layer defines the interfaces (ports) that the infrastructure layer implements, ensuring that business logic remains independent of technical concerns. + +### 2. Infra (Adapter Layer) - `src/infra/` + +- Adapters for ports: PostgreSQL (event store, projections), Temporal (workflow engine), Supabase (auth) +- Command/event pumps, observability hooks, and RLS enforcement live here +- Respects slice boundaries; no core leakage + +The Infra layer provides concrete implementations of the ports defined in the Core layer, connecting the business logic to external systems and technologies. + +### 3. Tooling Layer - `src/tools/` + +- Projection drift repair, snapshot verification, RLS linting, and devX CLI helpers +- Tied into CI for consistency enforcement and automated repair + +The Tooling layer provides developer utilities and CI/CD helpers to ensure the system remains consistent and secure. + +## Key Concepts Reference Table + +| Concept | Description | Implementation | +|---------|-------------|----------------| +| Aggregate | A cluster of domain objects treated as a single unit for data changes | Defined in `src/core/aggregates.ts` with a registry of aggregate types | +| Command | An intent to change the system state | Represented by the `Command` interface in `src/core/contracts.ts` | +| Command Bus | Routes commands to appropriate handlers | Implemented in `src/core/command-bus.ts` | +| Event | A record of something that happened in the system | Represented by the `Event` interface in `src/core/contracts.ts` | +| Event Bus | Routes events to interested handlers | Implemented in `src/core/event-bus.ts` | +| Event Store | Persists events as the source of truth | Implemented in `src/infra/pg/pg-event-store.ts` | +| Projection | Updates read models based on events | Implemented in various files under `src/core/*/read-models/` | +| Saga/Process | Orchestrates complex business processes | Defined by the `SagaDefinition` interface in `src/core/contracts.ts` | +| Temporal Activities | Durable operations that can be retried | +| Temporal Workflows | Orchestrates activities in a reliable way | +| Snapshot | Point-in-time capture of aggregate state | Supported by the event store for performance optimization | +| Read Model | Optimized representation of data for querying | Updated by projections based on events | +| Multi-tenancy | Support for multiple isolated customer environments | Implemented with tenant_id in commands, events, and database tables | +| Observability | Monitoring and tracing capabilities | Implemented in `src/infra/observability/` | + +## Design Decisions and Rationale + +The architecture of Intent is designed to address several key concerns: + +- **Reliability**: Event sourcing ensures no data loss, even under retries, crashes, or partial failures. +- **Scalability**: CQRS separates write and read models, allowing each to scale independently. +- **Maintainability**: Hexagonal architecture keeps the core logic clean and testable. +- **Security**: Multi-tenancy and RLS policies ensure proper data isolation. +- **Observability**: Comprehensive tracing and logging provide visibility into system behavior. + +Each of these architectural patterns and concepts is explored in more depth in dedicated pages: + +- [Domain Modeling & Aggregates](domain-modeling.md) +- [Event Sourcing Pattern & Event Store](event-sourcing.md) +- [CQRS Read Model Design & Projection Management](cqrs-projections.md) +- [Temporal Workflow Orchestration](temporal-workflows.md) +- [Multi-Tenancy Design Details](multi-tenancy-details.md) +- [Observability & Monitoring](observability-details.md) +- [Testing Strategies and CI](testing-strategies.md) \ No newline at end of file diff --git a/docs/architecture/cqrs-projections.md b/docs/architecture/cqrs-projections.md new file mode 100644 index 00000000..0181f650 --- /dev/null +++ b/docs/architecture/cqrs-projections.md @@ -0,0 +1,357 @@ +# CQRS Read Model Design & Projection Management + +This document provides an architectural deep-dive into how Intent implements Command Query Responsibility Segregation (CQRS) and manages projections for read models. + +## CQRS Pattern Overview + +Command Query Responsibility Segregation (CQRS) is a key architectural pattern in Intent that separates the write model (commands) from the read model (queries). This separation allows each side to be optimized for its specific purpose: + +- **Command Side (Write Model)**: Optimized for data consistency, business rule validation, and capturing state changes +- **Query Side (Read Model)**: Optimized for query performance, often using denormalized data structures + +In Intent, CQRS works hand-in-hand with Event Sourcing: events generated from commands are projected into read-optimized models that serve queries efficiently. + +## Command Side (Write Model) + +While the command side is covered in more detail in other sections (see [Domain Modeling](domain-modeling.md) and [Event Sourcing](event-sourcing.md)), it's important to understand how it relates to the read model. + +### Command Structure + +Commands in Intent are defined by the `Command` interface in `src/core/contracts.ts`: + +```typescript +export interface Command { + id: UUID; + tenant_id: UUID; + type: string; + payload: T; + status?: 'pending' | 'consumed' | 'processed' | 'failed'; + metadata?: Metadata; +} +``` + +### Command Flow + +The command handling process follows these steps: + +1. A command is dispatched through the Command Bus +2. The appropriate Command Handler processes the command +3. The Command Handler loads or creates the target Aggregate +4. The Aggregate's `handle` method processes the command and produces events +5. The events are persisted to the Event Store +6. The events are then projected to update read models + +This flow is implemented in the `dispatchCommand` function: + +```typescript +export async function dispatchCommand(cmd: Command): Promise { + try { + // Store the command + await pgCommandStore.upsert(cmd); + + // Route the command to a handler + const result: CommandResult = await routeCommand(cmd); + + // Update the command status + const infraStatus = result.status === 'success' ? 'consumed' : 'failed'; + await pgCommandStore.markStatus(cmd.id, infraStatus, result); + } catch (error: any) { + await pgCommandStore.markStatus(cmd.id, 'failed', {status: 'fail', error: error.message}); + throw error; + } +} +``` + +## Query Side (Read Model) + +The query side is where Intent's projection system comes into play. Projections transform events into read-optimized models that can be queried efficiently. + +### Projection Definition + +Projections in Intent are implemented as event handlers that update database tables or other storage mechanisms. They implement the `EventHandler` interface: + +```typescript +export interface EventHandler { + supportsEvent(event: Event): event is E; + on(event: E): Promise; +} +``` + +Each projection typically includes: + +1. A list of event types it handles +2. A table schema definition +3. Logic for transforming events into read model updates +4. Access control metadata (which roles can read the data) + +### Projection Implementation Example + +Here's an example of a projection implementation: + +```typescript +// Projection metadata defines the table schema and access control +export const projectionMeta = { + name: 'system_status', + eventTypes: [SystemEventType.TEST_EXECUTED], + tables: { + system_status: { + columns: { + id: 'uuid PRIMARY KEY', + tenant_id: 'uuid NOT NULL', + last_test_time: 'timestamp', + test_count: 'integer', + // ... other columns + }, + }, + }, + // Access control metadata + readAccess: { + roles: ['admin', 'user'], + }, +}; + +// The projection implementation +export function createSystemStatusProjection( + updater: ReadModelUpdaterPort +): EventHandler { + return { + supportsEvent(event): event is Event { + return projectionMeta.eventTypes.includes(event.type); + }, + + async on(event) { + // Extract data from the event + const { tenant_id, aggregateId, payload, metadata } = event; + + // Prepare data for the read model + const upsertData = { + id: aggregateId, + tenant_id, + last_test_time: new Date(), + test_count: 1, // This would be incremented in a real implementation + // ... other fields mapped from the event + }; + + // Update the read model + await updater.upsert(tenant_id, aggregateId, upsertData); + }, + }; +} +``` + +### Projection Registration + +Projections are registered and loaded dynamically, allowing the system to discover all projections at runtime: + +```typescript +export async function loadAllProjections(pool: DatabasePool): Promise { + const slices = await Promise.all([ + import('../../core/system/read-models/register').then(r => r.registerSystemProjections(pool)), + // Add more slices here as they are implemented + ]); + + return slices.flat(); +} +``` + +Each domain module typically has a `register.ts` file that exports all projections for that domain: + +```typescript +export async function registerSystemProjections(pool: DatabasePool): Promise { + const updater = createPgReadModelUpdater(pool); + + return [ + createSystemStatusProjection(updater), + // Other system projections... + ]; +} +``` + +### Projection Processing + +Events are processed by projections in the `projectEvents` function: + +```typescript +export async function projectEvents( + events: Event[], + pool: DatabasePool, +): Promise { + const handlers = await loadAllProjections(pool); + + for (const event of events) { + for (const h of handlers) { + if (!h.supportsEvent(event)) continue; + + try { + await traceSpan(`projection.handle.${event.type}`, { event }, () => + h.on(event), + ); + } catch (err) { + console.warn('Projection failed', { eventType: event.type, error: err }); + } + } + } +} +``` + +This function: +1. Loads all registered projections +2. For each event, finds projections that support the event type +3. Calls the projection's `on` method to update the read model +4. Wraps each projection call in a trace span for observability +5. Catches and logs errors to prevent one projection failure from affecting others + +## Schema Management + +One of the key challenges in maintaining projections is managing database schemas as projections evolve. Intent addresses this with a sophisticated schema management system. + +### Projection Schema Definition + +Each projection defines its expected database schema in its metadata: + +```typescript +export const projectionMeta = { + name: 'system_status', + tables: { + system_status: { + columns: { + id: 'uuid PRIMARY KEY', + tenant_id: 'uuid NOT NULL', + // ... other columns + }, + }, + }, + // ... +}; +``` + +### Schema Drift Detection + +Intent includes a tool to detect "schema drift" - discrepancies between the expected schema (defined in code) and the actual database schema: + +```typescript +// Simplified example from src/tools/projection-drift/check-projection-drift.ts +export async function checkProjectionDrift(pool: DatabasePool): Promise { + const projections = await loadAllProjections(pool); + const results: DriftResult[] = []; + + for (const projection of projections) { + const meta = getProjectionMeta(projection); + const expectedSchema = meta.tables; + + for (const [tableName, tableSchema] of Object.entries(expectedSchema)) { + const actualSchema = await getTableSchema(pool, tableName); + const drifts = compareSchemas(tableSchema, actualSchema); + + if (drifts.length > 0) { + results.push({ + projection: meta.name, + table: tableName, + drifts, + }); + } + } + } + + return results; +} +``` + +This tool is run in CI to catch schema drift early and can also be run locally during development. + +### Schema Repair + +When schema drift is detected, Intent provides a tool to automatically generate SQL migrations to fix the discrepancies: + +```typescript +// Simplified example from src/tools/projection-drift/repair-projection-drift.ts +export async function repairProjectionDrift(pool: DatabasePool): Promise { + const drifts = await checkProjectionDrift(pool); + const migrations: string[] = []; + + for (const drift of drifts) { + const sql = generateMigrationSql(drift); + migrations.push(sql); + + if (autoApply) { + await pool.query(sql); + } + } + + return migrations; +} +``` + +This approach allows for: +1. Automatic detection of schema changes +2. Generation of migration scripts +3. Optional automatic application of migrations +4. CI integration to prevent schema drift in production + +## Access Policies + +A key feature of Intent's projection system is the integration with access control. Each projection declares which roles can read its data: + +```typescript +export const projectionMeta = { + // ... + readAccess: { + roles: ['admin', 'user'], + }, +}; +``` + +These access policies are translated into PostgreSQL Row-Level Security (RLS) policies, ensuring that data access is controlled at the database level: + +```sql +-- Example generated RLS policy +CREATE POLICY "system_status_admin_user_read_policy" ON "system_status" + FOR SELECT + USING ( + current_setting('request.jwt.claims')->>'role' IN ('admin', 'user') + AND current_setting('request.jwt.claims')->>'tenant_id' = tenant_id::text + ); +``` + +This ensures that: +1. Only users with the appropriate roles can read the data +2. Users can only see data for their own tenant +3. These rules are enforced at the database level, not just in application code + +## Benefits of CQRS in Intent + +The CQRS approach in Intent provides several key benefits: + +1. **Optimized Models**: Each side (read and write) is optimized for its specific purpose +2. **Scalability**: Read and write sides can be scaled independently +3. **Performance**: Read models can be denormalized for query performance +4. **Flexibility**: New read models can be added without changing the write model +5. **Security**: Access control is built into the read model at the database level + +## Challenges and Mitigations + +CQRS also presents some challenges, which Intent addresses: + +1. **Eventual Consistency**: Read models may lag behind the write model + - **Mitigation**: Workflow engines ensure reliable processing of events to projections + +2. **Complexity**: Managing multiple models adds complexity + - **Mitigation**: Clear patterns and tooling simplify projection management + +3. **Schema Evolution**: Handling changes to event schemas and their impact on projections + - **Mitigation**: Schema drift detection and repair tools + +4. **Synchronization**: Ensuring read models are properly updated when events occur + - **Mitigation**: The `projectEvents` function is called as part of the command processing workflow + +## Extending the System + +To create a new projection in Intent: + +1. Define a new projection in a domain module's `read-models` directory +2. Define the table schema and access control in the projection metadata +3. Implement the `EventHandler` interface to process relevant events +4. Register the projection in the domain's `register.ts` file +5. Run the schema drift check and repair tool to create the necessary database tables + +This process ensures that new projections are consistent with the system's patterns and that the database schema stays in sync with the code. \ No newline at end of file diff --git a/docs/architecture/domain-modeling.md b/docs/architecture/domain-modeling.md new file mode 100644 index 00000000..1edcffb4 --- /dev/null +++ b/docs/architecture/domain-modeling.md @@ -0,0 +1,217 @@ +# Domain Modeling & Aggregates (Deep Dive) + +This document provides a detailed exploration of how domain modeling is implemented in Intent, expanding on the basic Aggregate concept introduced in the architecture overview. + +## Domain-Driven Design in Intent + +Intent follows Domain-Driven Design (DDD) principles to model complex business domains. The system organizes domain logic around business concepts, using aggregates as the primary building blocks. This approach ensures that business rules are enforced consistently and that the system accurately reflects the real-world domain it represents. + +## BaseAggregate: The Foundation + +At the core of Intent's domain modeling is the `BaseAggregate` abstract class, which provides the foundation for all domain aggregates: + +```typescript +export abstract class BaseAggregate { + abstract aggregateType: string; + version = 0; + + constructor(public id: UUID) {} + + // Schema versioning + static CURRENT_SCHEMA_VERSION = 1; + + // Abstract methods that must be implemented by concrete aggregates + abstract apply(event: any, isSnapshot?: boolean): void; + abstract handle(command: any): Event[]; + protected abstract applyUpcastedSnapshot(state: TState): void; + abstract extractSnapshotState(): TState; + + // Methods for snapshot handling + applySnapshotState(raw: any, incomingVersion?: number): void { /* ... */ } + toSnapshot(): Snapshot { /* ... */ } + static fromSnapshot>(this: new (id: UUID) => T, event: any): T { /* ... */ } +} +``` + +### Key Features of BaseAggregate + +1. **Generic State Type**: Uses a generic type parameter `TState` to define the shape of the aggregate's state +2. **Version Tracking**: Maintains a version number for optimistic concurrency control +3. **Schema Versioning**: Supports evolving the aggregate's schema over time via `CURRENT_SCHEMA_VERSION` +4. **Command Handling**: The `handle` method processes commands and produces events +5. **Event Application**: The `apply` method updates the aggregate's state based on events +6. **Snapshot Support**: Methods for creating and applying snapshots to optimize loading + +## Concrete Aggregate Example: SystemAggregate + +To illustrate how aggregates are implemented in practice, let's examine the `SystemAggregate` class: + +```typescript +export class SystemAggregate extends BaseAggregate { + public aggregateType = 'system'; + static CURRENT_SCHEMA_VERSION = 1; + + // State properties + id: UUID; + version = 0; + numberExecutedTests = 0; + + // Command handling + private readonly handlers: Record Event[]> = { + [SystemCommandType.LOG_MESSAGE]: this.handleLogMessage, + [SystemCommandType.EXECUTE_TEST]: this.handleExecuteTest, + // Other command handlers... + }; + + public handle(cmd: Command): Event[] { + const handler = this.handlers[cmd.type as SystemCommandType]; + if (!handler) { + throw new Error(`No handler for command type: ${cmd.type}`); + } + return handler.call(this, cmd); + } + + // Event application + public apply(event: Event, isNew = true): void { + switch (event.type) { + case SystemEventType.MESSAGE_LOGGED: + this.applyMessageLogged(event as Event); + break; + case SystemEventType.TEST_EXECUTED: + this.applyTestExecuted(event as Event); + break; + // Other event handlers... + } + + if (isNew) { + this.version++; + } + } + + // Specific command handlers + private handleLogMessage(cmd: Command): Event[] { + // Business logic and validation + return [ + createEvent({ + type: SystemEventType.MESSAGE_LOGGED, + aggregateId: this.id, + aggregateType: this.aggregateType, + payload: { message: cmd.payload.message }, + version: this.version + 1, + tenant_id: cmd.tenant_id, + }) + ]; + } + + // Specific event handlers + private applyTestExecuted(event: Event): void { + this.numberExecutedTests++; + } + + // Snapshot methods + protected applyUpcastedSnapshot(state: SystemSnapshotState): void { + this.numberExecutedTests = state.numberExecutedTests; + this.version = state.version; + } + + extractSnapshotState(): SystemSnapshotState { + return { + numberExecutedTests: this.numberExecutedTests, + version: this.version, + }; + } +} +``` + +### Key Aspects of the Implementation + +1. **State Definition**: The aggregate defines its state properties (e.g., `numberExecutedTests`) +2. **Command Handler Map**: Uses a map to route commands to specific handler methods +3. **Event Application**: Dispatches events to specific handlers based on event type +4. **Business Rules**: Enforces business rules in command handlers +5. **Version Management**: Increments the version when applying new events +6. **Snapshot Support**: Implements methods to create and apply snapshots + +## Aggregate Registry + +Intent maintains a registry of aggregate types, which allows the system to dynamically create and load aggregates based on their type: + +```typescript +export const AggregateRegistry: Record = { + system: SystemAggregate, + // Other aggregate types would be registered here +}; +``` + +This registry is crucial for the event sourcing mechanism, as it enables the system to: + +1. Create the correct aggregate type when loading from events or snapshots +2. Route commands to the appropriate aggregate type +3. Maintain a catalog of all available aggregate types in the system + +## Command Processing Flow + +When a command is received, it follows this processing flow: + +1. The command is routed to the appropriate command handler based on its type +2. The command handler loads or creates the target aggregate +3. The aggregate's `handle` method processes the command and produces events +4. The events are persisted to the event store +5. The events are applied to the aggregate to update its state + +This flow is implemented in the command handlers, such as `SystemCommandHandler`: + +```typescript +export class SystemCommandHandler implements CommandHandler> { + supportsCommand(cmd: Command): boolean { + return Object.values(SystemCommandType).includes(cmd.type as SystemCommandType); + } + + async handleWithAggregate(cmd: Command, aggregate: BaseAggregate): Promise[]> { + if (!(aggregate instanceof SystemAggregate)) { + throw new Error(`Expected SystemAggregate but got ${aggregate.constructor.name} for cmd: ${cmd.type}`); + } + return aggregate.handle(cmd); + } +} +``` + +## Schema Evolution and Versioning + +Intent supports evolving aggregate schemas over time through: + +1. **Schema Version Tracking**: Each aggregate class defines a `CURRENT_SCHEMA_VERSION` static property +2. **Snapshot Upcasting**: When loading a snapshot with an older schema version, the system can upcast it to the current version +3. **Backward Compatibility**: Events are designed to be backward compatible, with upcasters for handling schema changes + +This approach allows the system to evolve while maintaining compatibility with historical data. + +## Benefits of This Approach + +The domain modeling approach in Intent provides several benefits: + +1. **Encapsulation**: Business rules are encapsulated within the aggregate +2. **Consistency**: Aggregates ensure consistency boundaries are maintained +3. **Event Sourcing Integration**: The design works seamlessly with event sourcing +4. **Versioning Support**: Built-in support for schema evolution +5. **Clear Responsibility**: Each aggregate has a clear responsibility in the domain +6. **Testability**: Aggregates can be tested in isolation from infrastructure + +## Domain Modeling Principles + +Intent follows several key domain modeling principles: + +1. **Ubiquitous Language**: The code encourages using domain terminology consistently +2. **Bounded Contexts**: The system is organized into distinct domain slices +3. **Aggregates as Consistency Boundaries**: Each aggregate enforces its own consistency rules +4. **Rich Domain Model**: Business logic is in the domain model, not in application services +5. **Separation of Concerns**: Clear separation between domain logic and infrastructure + +## Integration with Other Patterns + +Domain modeling in Intent integrates with several other architectural patterns: + +1. **Event Sourcing**: Aggregates are the source of events +2. **CQRS**: Aggregates are part of the write model +3. **Temporal Workflows**: Complex processes may involve multiple aggregates +4. **Multi-tenancy**: Aggregates maintain tenant isolation throughout the system \ No newline at end of file diff --git a/docs/architecture/event-sourcing.md b/docs/architecture/event-sourcing.md new file mode 100644 index 00000000..8198a843 --- /dev/null +++ b/docs/architecture/event-sourcing.md @@ -0,0 +1,239 @@ +# Event Sourcing Pattern & Event Store + +This document covers how event sourcing is implemented in Intent and the architectural considerations behind it. + +## What is Event Sourcing? + +Event Sourcing is a fundamental architectural pattern in Intent. Instead of storing just the current state of the system, event sourcing persists all changes to the application state as a sequence of events. These events serve as the source of truth, and the current state can be derived by replaying these events. + +In traditional CRUD systems, you might update a record directly in a database. With event sourcing, you instead record the fact that a change occurred as an event, and then derive the current state by processing all events. + +## Event Structure + +Events in Intent are defined by the `Event` interface in `src/core/contracts.ts`: + +```typescript +export interface Event { + id: UUID; + tenant_id: UUID; + type: string; + payload: T; + aggregateId: UUID; + aggregateType: string; + version: number; + metadata?: Metadata; +} +``` + +Key components of an event: + +- **id**: Unique identifier for the event +- **tenant_id**: Supports multi-tenancy (every event belongs to a specific tenant) +- **type**: Describes what happened (e.g., `USER_REGISTERED`, `ORDER_PLACED`) +- **payload**: Contains the event data specific to the event type +- **aggregateId**: The ID of the aggregate this event applies to +- **aggregateType**: The type of the aggregate (e.g., `user`, `order`) +- **version**: Sequential version number for the aggregate (used for optimistic concurrency) +- **metadata**: Additional information like timestamps, correlation IDs, etc. + +## The Event Store + +The event store is the heart of an event-sourced system. In Intent, the event store is implemented in `src/infra/pg/pg-event-store.ts` using PostgreSQL. It provides several key capabilities: + +### 1. Appending Events + +When a command is processed and results in state changes, the event store appends new events to the aggregate's event stream: + +```typescript +async appendEvents(tenantId: UUID, events: Event[]): Promise { + // Validate events + // Ensure optimistic concurrency (check version) + // Insert events into the database +} +``` + +The event store ensures that events are appended atomically and that version numbers are sequential to maintain consistency. + +### 2. Loading Events + +To rebuild an aggregate's state, the event store can load all events for a specific aggregate: + +```typescript +async loadEvents( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID, + afterVersion?: number +): Promise { + // Query the database for events matching the criteria + // Optionally filter events after a specific version (for use with snapshots) + // Return the events in version order +} +``` + +### 3. Snapshot Management + +The event store also handles creating and loading snapshots: + +```typescript +async snapshotAggregate(tenantId: UUID, aggregate: BaseAggregate): Promise { + // Create a snapshot from the aggregate's current state + // Store the snapshot in the database +} + +async loadLatestSnapshot( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID +): Promise | null> { + // Query the database for the latest snapshot of the aggregate + // Return null if no snapshot exists +} +``` + +## Aggregate Rehydration + +One of the key operations in an event-sourced system is "rehydrating" an aggregate from its event history. Intent implements this process in the `loadAggregate` function: + +```typescript +export async function loadAggregate( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID +): Promise> { + // 1. Try to load the latest snapshot + const snapshot = await eventStore.loadLatestSnapshot(tenantId, aggregateType, aggregateId); + + // 2. Determine the starting version (0 or snapshot version) + const startingVersion = snapshot ? snapshot.version : 0; + + // 3. Load events after the snapshot version + const events = await eventStore.loadEvents( + tenantId, + aggregateType, + aggregateId, + startingVersion + ); + + // 4. Create or rehydrate the aggregate + let aggregate; + if (snapshot) { + // Create from snapshot + const AggregateClass = AggregateRegistry[aggregateType]; + aggregate = AggregateClass.fromSnapshot(snapshot); + } else if (events.length > 0) { + // Rehydrate from events + const AggregateClass = AggregateRegistry[aggregateType]; + aggregate = AggregateClass.rehydrate(events); + } else { + // Aggregate doesn't exist yet + throw new Error(`Aggregate ${aggregateType}:${aggregateId} not found`); + } + + // 5. Apply any events after the snapshot + if (snapshot) { + for (const event of events) { + aggregate.apply(event); + } + } + + return aggregate; +} +``` + +This process ensures that aggregates can be efficiently loaded from their event history, with snapshots providing optimization for aggregates with long histories. + +## Snapshots + +Snapshots are a performance optimization in event sourcing. Without snapshots, loading an aggregate would require replaying all events from the beginning of time, which could be slow for aggregates with many events. + +### How Snapshots Work + +1. A snapshot is a point-in-time capture of an aggregate's state +2. When loading an aggregate, the system first loads the latest snapshot +3. Then it only needs to apply events that occurred after the snapshot was taken +4. Snapshots are typically created periodically or after a certain number of events + +The `snapshotAggregate` function demonstrates how snapshots are created: + +```typescript +export async function snapshotAggregate( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID, +): Promise { + // Load the aggregate + const aggregate = await loadAggregate(tenantId, aggregateType, aggregateId); + + // Create and store the snapshot + await eventStore.snapshotAggregate(tenantId, aggregate); +} +``` + +This function is typically called by an activity as part of the command processing workflow. + +## Schema Evolution + +A key challenge in event sourcing is handling changes to event schemas over time. Intent addresses this through several mechanisms: + +### Event Upcasting + +When event schemas change, older events may need to be transformed to match the current schema. This process is called "upcasting": + +1. Events are stored with their original schema +2. When loading events, upcasters transform old events to the current schema +3. This ensures backward compatibility without modifying the original events + +Intent follows rules for event schema evolution as documented in ADR-017 (Event Upcasting): + +- Events should be designed for backward compatibility when possible +- Adding new optional fields is safe +- Removing fields requires upcasters +- Changing field types requires upcasters + +### Snapshot Upcasting + +Similar to events, snapshots also need to handle schema changes. Intent supports snapshot upcasting as documented in ADR-010 (Snapshot Upcasting): + +1. Snapshots include a schema version number +2. When loading a snapshot with an older schema version, the system upcasts it to the current version +3. This is implemented in the `applySnapshotState` method of `BaseAggregate` + +## Benefits of Event Sourcing in Intent + +Event sourcing provides several key benefits in the Intent architecture: + +1. **Complete Audit Trail**: Every change is recorded as an event, providing a complete history of all changes to the system. + +2. **Temporal Queries**: The ability to determine the state of the system at any point in time by replaying events up to that point. + +3. **Event Replay**: The system can be rebuilt by replaying events, which is useful for testing, debugging, and creating new projections. + +4. **Separation of Concerns**: Clear separation between write and read models (CQRS), allowing each to be optimized independently. + +5. **Business Insight**: Events represent business activities and can be analyzed to gain insights into system usage and behavior. + +## Challenges and Mitigations + +Event sourcing also presents some challenges, which Intent addresses: + +1. **Performance**: Loading aggregates requires replaying events, which can be slow for aggregates with many events. + - **Mitigation**: Intent uses snapshots to optimize loading time. + +2. **Schema Evolution**: Handling changes to event schemas over time. + - **Mitigation**: Intent supports upcasting for both events and snapshots. + +3. **Eventual Consistency**: Read models may lag behind the event store. + - **Mitigation**: Temporal workflows ensure reliable processing of events to projections. + +4. **Complexity**: Event sourcing adds complexity compared to traditional CRUD approaches. + - **Mitigation**: Intent provides a structured framework and clear patterns to manage this complexity. + +## Integration with Other Patterns + +Event sourcing in Intent integrates with several other architectural patterns: + +1. **CQRS**: Events from the event store are projected to read models for querying. +2. **Domain-Driven Design**: Events represent domain concepts and are used to rebuild aggregates. +3. **Temporal Workflows**: Complex processes are orchestrated using events and commands. +4. **Observability**: Events provide a basis for system monitoring and tracing. \ No newline at end of file diff --git a/docs/architecture/multi-tenancy-details.md b/docs/architecture/multi-tenancy-details.md new file mode 100644 index 00000000..231bdd6a --- /dev/null +++ b/docs/architecture/multi-tenancy-details.md @@ -0,0 +1,299 @@ +# Multi-Tenancy Design Details + +This document complements the basic multi-tenancy concept page with deeper implementation details on how Intent achieves robust tenant isolation across all layers of the architecture. + +## Multi-Tenancy Architecture + +Intent is designed from the ground up as a multi-tenant system, allowing a single deployment to serve multiple isolated customer environments (tenants). The implementation follows a "shared database, separate schemas" approach with comprehensive tenant isolation at multiple layers. + +## Tenant Identification + +Every tenant in the system is identified by a unique `tenant_id` (UUID), which serves as the foundation for isolation. This identifier is: + +1. Required in all commands and events +2. Stored in all database tables +3. Included in JWT tokens for authentication +4. Used to scope workflow execution +5. Part of composite primary keys in database tables + +This consistent use of `tenant_id` throughout the system ensures that tenant boundaries are maintained across all operations. + +## Tenant Isolation Layers + +Intent implements tenant isolation at multiple layers of the architecture: + +### 1. Database Layer + +The database layer is the foundation of tenant isolation in Intent. It uses several techniques to ensure data separation: + +#### Schema Approach + +Intent uses a "shared database, shared tables" approach where every table includes a `tenant_id` column. This approach offers a good balance between resource efficiency and isolation: + +```sql +CREATE TABLE "public"."aggregates" ( + "id" uuid NOT NULL, + "tenant_id" uuid NOT NULL, + "type" text NOT NULL, + "version" int4 NOT NULL, + "snapshot" jsonb, + "created_at" timestamptz NOT NULL, + "schema_version" int4 NOT NULL DEFAULT 1 +); + +ALTER TABLE "public"."aggregates" ADD CONSTRAINT "aggregates_pkey" PRIMARY KEY ("id", "tenant_id"); +``` + +Key aspects of the database schema design: + +1. **Required Tenant ID**: The `tenant_id` column is defined as `NOT NULL` to ensure every record belongs to a tenant +2. **Composite Primary Keys**: Primary keys include `tenant_id` to prevent ID collisions across tenants +3. **Indexing**: Indexes on `tenant_id` improve query performance for tenant-specific data + +#### Row-Level Security (RLS) + +One of the most powerful features of Intent's multi-tenancy is the use of PostgreSQL's Row-Level Security (RLS) to enforce tenant isolation at the database level: + +```sql +-- Example RLS policy generated for a table +CREATE POLICY "tenant_isolation_policy" ON "system_status" + USING (tenant_id::text = current_setting('request.jwt.claims')->>'tenant_id'); +``` + +This is implemented in the code that generates RLS policies: + +```typescript +// From src/infra/projections/genRlsSql.ts +// Add tenant_id check for multi-tenant tables if not already present +if (!hasTenantCheck && hasMultiTenancy) { + // We'll add the tenant check, assuming the table has a tenant_id column + sqlCondition = `${sqlCondition} AND current_setting('request.jwt.claims', true)::json->>'tenant_id' = tenant_id::text`; +} +``` + +The critical benefit of RLS is that it ensures tenant isolation even if application code fails to filter by `tenant_id`. The database itself will enforce the boundary, providing a robust security layer. + +#### Session Context + +Intent sets a tenant context at the database session level: + +```typescript +// From src/infra/pg/pg-command-store.ts +private async setTenantContext(client: PoolClient, tenantId: UUID): Promise { + await client.query(`SET LOCAL app.tenant_id = '${tenantId}'`); +} +``` + +This allows database functions and triggers to access the current tenant context, which can be useful for audit logging, automatic tenant filtering, and other cross-cutting concerns. + +### 2. Domain Layer + +The domain layer enforces tenant isolation through several mechanisms: + +#### Commands and Events + +Both commands and events require a `tenant_id` field: + +```typescript +// From src/core/contracts.ts +export interface Command { + id: UUID; + tenant_id: UUID; + type: string; + payload: T; + status?: 'pending' | 'consumed' | 'processed' | 'failed'; + metadata?: Metadata; +} + +export interface Event { + id: UUID; + tenant_id: UUID; + type: string; + payload: T; + aggregateId: UUID; + aggregateType: string; + version: number; + metadata?: Metadata; +} +``` + +The command bus enforces tenant consistency between the command and its payload: + +```typescript +// From src/core/command-bus.ts +const cmdTenant = cmd.tenant_id; +const payloadTenant = (cmd.payload as any)?.tenantId; + +if (payloadTenant && payloadTenant !== cmdTenant) { + throw new Error(`[Command-bus] Mismatch between command.tenant_id and payload.tenantId`); +} +``` + +This guard prevents cross-tenant misuse by ensuring that a command's payload tenant matches the command's tenant. + +#### Aggregates and Projections + +Aggregates and projections maintain tenant isolation when processing events and updating read models: + +```typescript +// From src/core/system/read-models/system-status.projection.ts +async on(event) { + const { tenant_id, aggregateId, payload, metadata } = event; + + if (!tenant_id || !aggregateId || !payload) { + throw new Error(`[System-Status-Projection] Invalid event ${event.type}. Missing tenant_id, aggregateId, or payload.`); + } + + const upsertData = { + id: aggregateId, + tenant_id, + // ... other fields + }; + + await updater.upsert(tenant_id, aggregateId, upsertData); +} +``` + +This ensures that events from one tenant cannot affect the state of another tenant's aggregates or read models. + +### 3. API Layer + +The API layer is where tenant context is established from incoming requests: + +#### JWT-Based Authentication + +Intent extracts tenant information from JWT tokens: + +```typescript +// From src/infra/supabase/edge-functions/command.ts +// Extract the tenant_id from the JWT claims +const tenantId = user.app_metadata?.tenant_id; +if (!tenantId) { + return new Response(JSON.stringify({ error: 'Missing tenant_id claim' }), { + status: 403, + headers: { 'Content-Type': 'application/json' } + }); +} +``` + +This tenant ID from the JWT becomes the source of truth for all actions performed by the request. + +#### Edge Functions + +In a serverless environment, Edge Functions can extract the tenant ID from the JWT and use it to scope all operations: + +```typescript +// Example Edge Function that extracts tenant_id from JWT +export async function handleRequest(req: Request) { + // Get the JWT from the Authorization header + const authHeader = req.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return new Response('Unauthorized', { status: 401 }); + } + + const token = authHeader.split(' ')[1]; + const decoded = verifyJWT(token); + + // Extract tenant_id from the JWT claims + const tenantId = decoded.claims.tenant_id; + if (!tenantId) { + return new Response('Missing tenant_id claim', { status: 403 }); + } + + // Use the tenant_id for all operations + // ... +} +``` + +### 4. Processing Layer + +The processing layer ensures that workflows and messaging are tenant-scoped: + +#### Temporal Workflows + +Workflow execution is scoped by tenant: + +```typescript +// From src/infra/temporal/workflow-router.ts +const {tenant_id} = cmd; +const {aggregateType, aggregateId} = cmd.payload; +const workflowId = this.getAggregateWorkflowId(tenant_id, aggregateType, aggregateId); + +// Execute with tenant tags for observability +const result = await this.client.workflow.execute(processCommand, { + taskQueue: this.taskQueue, + workflowId, + searchAttributes: { + tenantId: [`${tenant_id}`], + }, + args: [tenant_id, aggregateType, aggregateId, cmd], +}); +``` + +This ensures that: +1. Workflow IDs include the tenant ID, preventing cross-tenant interference +2. Search attributes include tenant information for filtering in the Temporal UI +3. The tenant ID is passed to the workflow for use in all activities + +#### Event Publication and Subscription + +Events are published to tenant-specific channels: + +```typescript +// From src/infra/supabase/supabase-publisher.ts +.channel(`tenant-${event.tenant_id}`) +``` + +This ensures that subscribers only receive events for their specific tenant, preventing information leakage across tenant boundaries. + +## Testing & Verification + +Intent includes integration tests specifically designed to verify tenant isolation: + +```typescript +// From src/infra/integration-tests/projection.integration.test.ts +// Verify tenant isolation +expect(result.rows.every((row: { tenant_id: any; }) => row.tenant_id === tenantId)).toBe(true); +expect(result.rows.every((row: { tenant_id: any; }) => row.tenant_id !== tenantId2)).toBe(true); +``` + +These tests ensure that: +1. Data created for one tenant is only visible to that tenant +2. Operations for one tenant do not affect data for other tenants +3. RLS policies correctly enforce tenant boundaries + +## Trade-offs and Considerations + +Intent currently uses a single database with RLS as opposed to separate databases per tenant. This approach has several trade-offs: + +### Advantages + +1. **Simpler Schema Management**: A single schema to maintain and evolve +2. **Resource Efficiency**: Better utilization of database resources +3. **Operational Simplicity**: Easier backup, monitoring, and scaling +4. **Feature Parity**: All tenants get the same features simultaneously + +### Challenges + +1. **Noisy Neighbor Risk**: One tenant's heavy usage could impact others +2. **Security Complexity**: RLS must be correctly implemented everywhere +3. **Query Performance**: Filtering by tenant_id adds overhead +4. **Blast Radius**: Database issues affect all tenants + +Intent mitigates these challenges through: +1. Comprehensive testing of tenant isolation +2. Automated RLS policy generation and verification +3. Performance optimization of tenant-filtered queries +4. Careful resource allocation and monitoring + +## Extending Multi-Tenancy + +When implementing new features in Intent, developers should: + +1. Ensure all database tables include a `tenant_id` column +2. Include tenant_id in all commands and events +3. Verify that RLS policies are generated for new tables +4. Test cross-tenant isolation for new features +5. Consider tenant-specific resource limits if needed + +By following these guidelines, the system maintains its strong tenant isolation as it evolves. \ No newline at end of file diff --git a/docs/architecture/observability-details.md b/docs/architecture/observability-details.md new file mode 100644 index 00000000..25352155 --- /dev/null +++ b/docs/architecture/observability-details.md @@ -0,0 +1,325 @@ +# Observability & Monitoring (Detailed) + +This document provides a deep dive into how Intent implements observability beyond the basics, covering the technical details of tracing, logging, and monitoring throughout the system. + +## OpenTelemetry Integration + +Intent uses OpenTelemetry as its observability framework, providing distributed tracing capabilities across all components of the system. The implementation is both simple and powerful: + +```typescript +// src/infra/observability/otel-trace-span.ts +import { trace } from '@opentelemetry/api'; + +const tracer = trace.getTracer('infra'); + +export async function traceSpan( + name: string, + attributes: Record, + fn: () => Promise +): Promise { + console.log(`[otel-traceSpan] Starting span: ${name}`, attributes); + return tracer.startActiveSpan(name, { attributes }, async (span) => { + try { + return await fn(); + } catch (error) { + if (error instanceof Error) { + span.recordException(error); + } else { + span.recordException({ message: String(error) }); + } + throw error; + } finally { + span.end(); + } + }); +} +``` + +This `traceSpan` helper function is used throughout the codebase to wrap operations in trace spans. It provides: + +1. **Automatic Error Recording**: Any exceptions thrown during the operation are automatically recorded in the span +2. **Proper Span Lifecycle**: Spans are always ended, even if an error occurs +3. **Attribute Enrichment**: Operations can include relevant attributes for context +4. **Simplified API**: A clean, consistent interface for creating spans + +### Usage Examples + +The `traceSpan` helper is used in various parts of the system: + +```typescript +// Command handling +await traceSpan('command.handle', { command }, async () => { + // Command handling logic +}); + +// Event processing +await traceSpan('event.process', { event }, async () => { + // Event processing logic +}); + +// Database operations +await traceSpan('db.query', { query, params }, async () => { + // Database query execution +}); +``` + +This consistent approach ensures that all major flows in the system are properly instrumented, providing end-to-end traceability. + +## Workflow Tracing + +One of the most innovative aspects of Intent's observability is how it handles tracing in workflow engine. Since workflows must be deterministic (the same inputs must produce the same outputs), traditional tracing can be challenging. + +Intent solves this with a clever signal-based approach: + +```typescript +// From src/infra/temporal/workflows/processCommand.ts +const obsTraceSignal = defineSignal<[{ span: string; data?: Record }]>('obs.trace'); + +setHandler(obsTraceSignal, async ({span, data}) => { + await emitObservabilitySpan(span, data); +}); +``` + +The `emitObservabilitySpan` activity creates a span without executing any code: + +```typescript +// From src/infra/temporal/activities/observabilityActivities.ts +export async function emitObservabilitySpan(span: string, data?: Record) { + await traceSpan(span, data || {}, async () => {}); +} +``` + +This pattern allows: + +1. **Non-intrusive Tracing**: Workflows can be traced without modifying their deterministic logic +2. **External Observability**: External systems can send signals to workflows to create spans +3. **Workflow Correlation**: Traces can be correlated with workflow execution +4. **Long-running Process Visibility**: Even workflows that run for days or weeks can emit trace markers + +### Workflow Tracing in Action + +When a workflow is running, it can receive an `obs.trace` signal to create a span: + +```typescript +// Example of sending a trace signal to a workflow +await client.workflow.signalWithStart(processCommand, { + taskQueue: 'intent-tasks', + workflowId, + signal: obsTraceSignal, + signalArgs: [{ span: 'workflow.milestone.reached', data: { milestone: 'payment-processed' } }], + args: [command], +}); +``` + +This creates a trace span without affecting the deterministic execution of the workflow, providing visibility into the workflow's progress. + +## Projection Tracing + +Projections are a critical part of the CQRS pattern in Intent, and they are fully instrumented for observability: + +```typescript +// From src/infra/projections/projectEvents.ts +for (const event of events) { + for (const h of handlers) { + if (!h.supportsEvent(event)) continue; + + try { + await traceSpan(`projection.handle.${event.type}`, { event }, () => + h.on(event), + ); + } catch (err) { + console.warn('Projection failed', { eventType: event.type, error: err }); + } + } +} +``` + +This provides: + +1. **Per-Event Tracing**: Each projection handler execution is wrapped in a span +2. **Event Context**: The span includes the event data for context +3. **Error Tracking**: Failed projections are logged with detailed error information +4. **Performance Monitoring**: Span durations reveal how long each projection takes to process events + +This level of detail is invaluable for debugging projection issues and understanding the performance characteristics of the read model updates. + +## Logging + +While the note focuses primarily on tracing, Intent also includes a structured logging system: + +```typescript +// Example of structured logging with context +logger.info('Command processed', { + commandId: command.id, + commandType: command.type, + tenantId: command.tenant_id, + correlationId: command.metadata?.correlationId, + duration: performance.now() - startTime, +}); +``` + +Key aspects of the logging system: + +1. **Structured Format**: Logs are structured (typically JSON) for easy parsing and analysis +2. **Context Enrichment**: Logs include relevant context like tenant IDs and correlation IDs +3. **Log Levels**: Different log levels (debug, info, warn, error) for appropriate verbosity +4. **Correlation with Traces**: Logs include trace identifiers for correlation with distributed traces + +The `LoggerPort` interface provides a consistent logging API across the system, which can be implemented by various logging backends (e.g., pino, winston). + +## Testing Observability + +Intent takes the unusual but valuable step of testing its observability instrumentation. This ensures that the observability features themselves are working correctly: + +```typescript +// From src/infra/observability/otel-test-tracer.ts +import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; + +export const memoryExporter = new InMemorySpanExporter(); + +const provider = new NodeTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(memoryExporter)], +}); + +provider.register({ contextManager }); +``` + +Integration tests verify that spans are created correctly: + +```typescript +// From src/infra/integration-tests/otel.test.ts +it('emits a projection.handle span', async () => { + memoryExporter.reset(); + + const evt: Event = { + id: randomUUID(), + type: 'testExecuted', + // ... other fields + }; + + await projectEvents([evt], pool); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans.length).toBeGreaterThan(0); + expect(spans[0].name).toBe('projection.handle.testExecuted'); +}); +``` + +This approach: + +1. **Verifies Instrumentation**: Ensures that spans are created as expected +2. **Prevents Regressions**: Catches changes that might break observability +3. **Documents Expected Behavior**: Shows what spans should be created in different scenarios + +## Observability Patterns + +Intent follows several key patterns for effective observability: + +### Span Naming Conventions + +Consistent span naming makes it easier to understand and query traces: + +- `projection.handle.{eventType}` for projection handlers +- `command.handle.{commandType}` for command handlers +- `workflow.{workflowName}.{activity}` for workflow activities +- `db.{operation}` for database operations + +### Attribute Enrichment + +Spans are enriched with relevant attributes to provide context: + +```typescript +await traceSpan('command.handle', { + commandId: command.id, + commandType: command.type, + tenantId: command.tenant_id, + aggregateId: command.payload.aggregateId, + aggregateType: command.payload.aggregateType, +}, async () => { + // Command handling logic +}); +``` + +These attributes make it possible to filter and analyze traces based on various dimensions. + +### Error Tracking + +Errors are automatically recorded in spans, providing valuable debugging information: + +```typescript +try { + return await fn(); +} catch (error) { + if (error instanceof Error) { + span.recordException(error); + } else { + span.recordException({ message: String(error) }); + } + throw error; +} +``` + +This ensures that when something goes wrong, the trace contains detailed error information. + +### Correlation IDs + +Every workflow and request carries a correlationId (from Metadata) which is included in logs and traces: + +```typescript +// Example of propagating correlation IDs +const correlationId = command.metadata?.correlationId || randomUUID(); +const childCommand = { + // ...command properties + metadata: { + ...command.metadata, + correlationId, + causationId: command.id, + }, +}; +``` + +This allows for tracking related operations across system boundaries. + +## Benefits of Intent's Observability + +The comprehensive observability in Intent provides several key benefits: + +1. **Debugging**: When issues occur, traces provide a detailed timeline of what happened, making it easier to identify the root cause. + +2. **Performance Tuning**: Span durations reveal performance bottlenecks, showing which operations are taking the most time. + +3. **System Understanding**: Traces provide a visual representation of how the system works, helping new developers understand the flow of operations. + +4. **Operational Visibility**: Patterns in traces can reveal operational issues, such as increased latency or error rates. + +5. **Cross-Component Correlation**: Traces span across system boundaries, showing how different components interact. + +## Extending Observability + +To add observability to new components in Intent: + +1. **Use the `traceSpan` Helper**: Wrap operations in `traceSpan` calls to create spans. + +2. **Follow Naming Conventions**: Use consistent span names that follow the established patterns. + +3. **Include Relevant Attributes**: Add attributes that provide context for the operation. + +4. **Propagate Context**: Ensure that trace context is propagated across async boundaries. + +5. **Test Instrumentation**: Write tests that verify spans are created as expected. + +By following these guidelines, new components will maintain the same level of observability as the rest of the system. + +## Observability Configuration + +Intent's observability can be configured through environment variables: + +``` +# Observability configuration +LOG_LEVEL=info # Log level (debug, info, warn, error) +LOG_ERRORS_TO_STDERR=false # Whether to log errors to stderr +OTEL_EXPORTER_OTLP_ENDPOINT= # OpenTelemetry collector endpoint +OTEL_RESOURCE_ATTRIBUTES= # Resource attributes for spans +``` + +This allows for tuning the verbosity and destination of logs and traces based on the environment (development, staging, production). \ No newline at end of file diff --git a/docs/architecture/temporal-workflows.md b/docs/architecture/temporal-workflows.md new file mode 100644 index 00000000..bb287162 --- /dev/null +++ b/docs/architecture/temporal-workflows.md @@ -0,0 +1,364 @@ +# Temporal Workflow Orchestration (Deep Dive) + +This document elaborates on Intent's use of Temporal for orchestrating domain workflows, building on the Saga concept introduced in the core concepts documentation. + +## Why Temporal in Intent? + +[Temporal](https://temporal.io/) is a workflow orchestration platform that Intent uses to manage complex, long-running business processes. It was chosen for several key reasons: + +1. **Durability**: Workflows continue executing even if the process or machine fails +2. **Reliability**: Automatic retries for failed activities +3. **Visibility**: Provides visibility into workflow execution status and history +4. **Scalability**: Workers can be scaled independently to handle load +5. **Long-running processes**: Supports processes that can run for days, weeks, or even longer + +These characteristics make Temporal an ideal fit for implementing event-sourced systems where reliability and consistency are paramount. + +## Key Temporal Concepts in Intent + +### Workflows + +Workflows in Temporal are durable, fault-tolerant functions that orchestrate activities. They are defined as code but executed as persistent, resumable programs that can span long periods of time. + +In Intent, workflows are implemented in `src/infra/temporal/workflows/` and are responsible for orchestrating the command-event-projection cycle. + +### Activities + +Activities are the building blocks of workflows. They are individual tasks that workflows orchestrate. Activities can be retried independently if they fail, without having to restart the entire workflow. + +In Intent, activities are defined in `src/infra/temporal/activities/` and serve as the bridge between Temporal workflows and the core domain logic. + +### Workers + +Workers are processes that execute workflow and activity code. They poll task queues for work and execute the corresponding code. + +Intent includes a worker implementation in `src/worker.ts` that registers workflows and activities with Temporal. The worker can be started with: + +```bash +npm run dev:worker aggregates # starts the aggregates worker +npm run dev:worker sagas # starts the sagas worker +``` + +## The processCommand Workflow + +The most important workflow in Intent is the `processCommand` workflow, which encapsulates the full command→event→projection cycle: + +```typescript +export async function processCommand(command: Command): Promise { + // 1. Load the aggregate + const aggregate = await activities.loadAggregate( + command.tenant_id, + command.aggregateType, + command.aggregateId + ); + + // 2. Generate events for the command + const result = await activities.routeCommand(command); + if (result.status !== 'success') { + return result; + } + + // 3. Apply the events to the aggregate + await activities.applyEvents( + command.tenant_id, + command.aggregateType, + command.aggregateId, + result.events + ); + + // 4. Project the events to read models + await activities.projectEvents(result.events); + + // 5. Route the events to interested handlers (e.g., sagas) + for (const event of result.events) { + await activities.routeEvent(event); + } + + return result; +} +``` + +This workflow ensures that all steps in command processing are completed reliably, with automatic retries for any failures. It maintains atomicity across the entire process, ensuring that either all steps complete successfully or none do. + +## Core Activities + +The activities used by workflows are the bridge between Temporal and the core domain logic. The key activities include: + +### 1. loadAggregate + +Loads an aggregate from the event store, optionally using a snapshot for optimization: + +```typescript +export async function loadAggregate( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID +): Promise> { + // 1. Try to load the latest snapshot + const snapshot = await eventStore.loadLatestSnapshot(tenantId, aggregateType, aggregateId); + + // 2. Determine the starting version + const startingVersion = snapshot ? snapshot.version : 0; + + // 3. Load events after the snapshot version + const events = await eventStore.loadEvents( + tenantId, + aggregateType, + aggregateId, + startingVersion + ); + + // 4. Create or rehydrate the aggregate + let aggregate; + if (snapshot) { + // Create from snapshot + const AggregateClass = AggregateRegistry[aggregateType]; + aggregate = AggregateClass.fromSnapshot(snapshot); + } else if (events.length > 0) { + // Rehydrate from events + const AggregateClass = AggregateRegistry[aggregateType]; + aggregate = AggregateClass.rehydrate(events); + } else { + // Aggregate doesn't exist yet + throw new Error(`Aggregate ${aggregateType}:${aggregateId} not found`); + } + + // 5. Apply any events after the snapshot + if (snapshot) { + for (const event of events) { + aggregate.apply(event); + } + } + + return aggregate; +} +``` + +### 2. routeCommand + +Routes commands to appropriate handlers in the domain: + +```typescript +export async function routeCommand(command: Command): Promise { + if (!router) { + router = await WorkflowRouter.create(); + } + return router.handle(command); +} +``` + +### 3. applyEvents + +Applies events to an aggregate and saves them to the event store: + +```typescript +export async function applyEvents( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID, + events: Event[], +): Promise { + // 1. Validate events + // 2. Append events to the event store + await eventStore.appendEvents(tenantId, events); + + // 3. Optionally create a snapshot + if (shouldCreateSnapshot(events)) { + await snapshotAggregate(tenantId, aggregateType, aggregateId); + } +} +``` + +### 4. projectEvents + +Projects events to read models: + +```typescript +export async function projectEvents(events: Event[]): Promise { + await projectEventsInfra(events, projectionPool); +} +``` + +### 5. routeEvent + +Routes events to interested handlers (e.g., Saga/PMs): + +```typescript +export async function routeEvent(event: Event): Promise { + if (!router) { + router = await WorkflowRouter.create(); + } + return router.on(event); +} +``` + +## Workflow Router + +The `WorkflowRouter` class is a key component that ensures each aggregate's commands go to the correct workflow. It uses the aggregate ID as part of the Temporal workflow ID, ensuring single-threaded command processing per aggregate: + +```typescript +export class WorkflowRouter { + private commandHandlers: CommandHandler[] = []; + private eventHandlers: EventHandler[] = []; + + static async create(): Promise { + // Load all command and event handlers + const router = new WorkflowRouter(); + await router.initialize(); + return router; + } + + async handle(command: Command): Promise { + // Find the appropriate command handler + const handler = this.findCommandHandler(command); + if (!handler) { + return { status: 'fail', error: `No handler for command type: ${command.type}` }; + } + + // Load the aggregate + const aggregate = await loadAggregate( + command.tenant_id, + command.aggregateType, + command.aggregateId + ); + + // Handle the command + try { + const events = await handler.handleWithAggregate(command, aggregate); + return { status: 'success', events }; + } catch (error: any) { + return { status: 'fail', error: error.message }; + } + } + + async on(event: Event): Promise { + // Find all event handlers that support this event + const handlers = this.findEventHandlers(event); + + // Process the event with each handler + for (const handler of handlers) { + await handler.on(event); + } + } + + // Helper methods... +} +``` + +This router ensures that: + +1. Commands are routed to the appropriate handler +2. Events are routed to all interested handlers +3. Aggregate consistency is maintained (commands for the same aggregate are processed sequentially) + +## Workers + +Workers are the processes that execute workflow and activity code. Intent's worker implementation is in `src/worker.ts`: + +```typescript +async function startWorker() { + const connection = await NativeConnection.connect({ + address: process.env.TEMPORAL_ADDRESS || 'localhost:7233', + }); + + const worker = await Worker.create({ + connection, + namespace: 'default', + taskQueue: 'intent-tasks', + workflowsPath: require.resolve('./infra/temporal/workflows'), + activities: { + loadAggregate, + routeCommand, + applyEvents, + projectEvents, + routeEvent, + // Other activities... + }, + }); + + await worker.run(); +} + +startWorker().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +This worker registers all workflows and activities with the Temporal server, making them available for execution. + +## Benefits and Trade-offs + +### Benefits + +1. **Reliability**: Temporal will retry failed activities, and preserve workflow state across failures +2. **Observability**: Temporal Web UI provides visibility into workflow execution, making debugging easier +3. **Saga Pattern**: Temporal is a natural fit for implementing the Saga pattern for complex business processes +4. **Scalability**: Workers can be scaled independently to handle increased load +5. **Versioning**: Temporal supports versioning of workflow and activity code, making it easier to evolve the system + +### Trade-offs + +1. **Determinism**: Workflow code must be deterministic to ensure consistent replay, which can be challenging +2. **Learning Curve**: Temporal has a unique programming model that requires understanding +3. **Complexity**: Adding Temporal introduces another system to manage and monitor +4. **State Size**: Large workflow states can impact performance + +## Integration with Other Patterns + +Temporal workflow orchestration in Intent integrates seamlessly with other architectural patterns: + +1. **Event Sourcing**: Workflows orchestrate the loading and applying of events +2. **CQRS**: Workflows ensure that commands are processed and events are projected to read models +3. **Domain-Driven Design**: Workflows respect domain boundaries and aggregate consistency rules +4. **Multi-tenancy**: Workflow IDs include tenant information to maintain isolation + +## Observability + +Intent includes comprehensive observability features for Temporal workflows: + +1. **Tracing**: Activities use `traceSpan` for distributed tracing +2. **Logging**: Extensive logging throughout activities and workflows +3. **Error Handling**: Structured error handling and reporting +4. **Temporal Web UI**: Provides visibility into workflow execution status and history + +## Adding New Workflows and Activities + +To extend Intent with new workflows or activities: + +1. **Define a new activity**: Create a new function in `src/infra/temporal/activities/` and register it in the worker +2. **Define a new workflow**: Create a new function in `src/infra/temporal/workflows/` that orchestrates activities +3. **Register with the worker**: Update `src/worker.ts` to include the new workflow or activity +4. **Invoke the workflow**: Use the Temporal client to start the workflow + +For example, to add a new workflow for a custom long-running saga: + +```typescript +// Define the workflow +export async function customSagaWorkflow(input: CustomSagaInput): Promise { + // Orchestrate activities to implement the saga + const result1 = await activities.firstStep(input); + const result2 = await activities.secondStep(result1); + await activities.finalStep(result2); +} + +// Register with the worker +const worker = await Worker.create({ + // ... + workflowsPath: require.resolve('./infra/temporal/workflows'), + // ... +}); + +// Invoke the workflow +const client = new Client({ + connection, + namespace: 'default', +}); +const handle = await client.workflow.start(customSagaWorkflow, { + taskQueue: 'intent-tasks', + workflowId: `custom-saga-${input.id}`, + args: [input], +}); +``` + +This extensibility allows Intent to support a wide range of complex business processes while maintaining reliability and observability. \ No newline at end of file diff --git a/docs/architecture/testing-strategies.md b/docs/architecture/testing-strategies.md new file mode 100644 index 00000000..d3349597 --- /dev/null +++ b/docs/architecture/testing-strategies.md @@ -0,0 +1,442 @@ +# Testing Strategies and CI + +This document describes the testing approach of Intent for those maintaining or extending the system. It covers the multi-level testing methodology, key testing patterns, and integration with CI/CD pipelines. + +## Testing Philosophy + +Intent follows a testing strategy that covers multiple levels of the application, from unit tests of individual components to integration tests of the entire system. This approach ensures that both the individual parts and the system as a whole function correctly and reliably. + +The testing strategy is designed to: + +1. **Verify Correctness**: Ensure that the system behaves as expected +2. **Prevent Regressions**: Catch issues before they reach production +3. **Document Behavior**: Serve as living documentation of how the system works +4. **Support Refactoring**: Enable safe refactoring and evolution of the codebase +5. **Validate Cross-Cutting Concerns**: Verify multi-tenancy, observability, and other architectural aspects + +## Testing Levels + +Intent implements a multi-level testing approach, with different types of tests focusing on different aspects of the system. + +### Unit Tests + +Unit tests focus on testing individual components in isolation, typically mocking or stubbing dependencies. In Intent, unit tests are organized in `__tests__` directories alongside the code they test. + +Key unit test examples: + +#### Base Component Tests + +These tests verify the core abstractions and base classes: + +```typescript +// From src/core/base/__tests__/aggregate.test.ts +describe('BaseAggregate', () => { + describe('toSnapshot', () => { + it('should create a snapshot with the correct structure', () => { + // Arrange + const aggregateId = 'test-aggregate-id'; + const aggregate = new ExampleAggregate(aggregateId); + + // Apply some events to change the state + aggregate.apply(ExampleAggregate.createNameChangedEvent(aggregateId, 'Test Aggregate')); + aggregate.apply(ExampleAggregate.createCounterIncrementedEvent(aggregateId, 5)); + aggregate.apply(ExampleAggregate.createItemAddedEvent(aggregateId, 'item1')); + + // Act + const snapshot = aggregate.toSnapshot(); + + // Assert + expect(snapshot).toBeDefined(); + expect(snapshot.id).toBe(aggregateId); + expect(snapshot.type).toBe('example'); + // ... more assertions + }); + }); + + // ... more test cases +}); +``` + +This test follows the Arrange-Act-Assert pattern and tests a specific functionality (snapshot creation) in isolation. + +#### Domain Component Tests + +These tests verify domain-specific implementations: + +```typescript +// From src/core/system/__tests__/system.aggregate.test.ts +test('should execute a test and increment numberExecutedTests', () => { + const command = { + id: 'test-id', + tenant_id: 'test-tenant', + type: SystemCommandType.EXECUTE_TEST, + metadata: { + userId: 'test-user-1', + role: 'tester', + timestamp: new Date() + }, + payload: { + testId: 'test-id', + testName: 'Test Name' + } as ExecuteTestPayload + }; + + const events = systemAggregate.handle(command); + expect(events).toHaveLength(1); + expect(events[0].type).toBe(SystemEventType.TEST_EXECUTED); + // ... more assertions + systemAggregate.apply(events[0]); + expect(systemAggregate.numberExecutedTests).toBe(1); +}); +``` + +This test verifies that the domain logic (executing a test and incrementing the counter) works correctly. + +### Integration Tests + +Integration tests verify that different components work together correctly. Intent has extensive integration tests in the `src/infra/integration-tests` directory: + +#### Command Processing Tests + +These tests verify the end-to-end flow of command processing: + +```typescript +// From src/infra/integration-tests/commands.test.ts +test('can dispatch a command and get events', async () => { + const cmd = { + id: uuidv4(), + tenant_id: tenantId, + type: SystemCommandType.EXECUTE_TEST, + payload: { + systemId: systemId, + testId: uuidv4(), + testName: 'integration-test', + }, + metadata: { + userId: testerId, + role: 'tester', + timestamp: new Date() + } + }; + + const result = await dispatchCommand(cmd); + + expect(result.status).toBe('success'); + expect(result.events).toHaveLength(1); + expect(result.events[0].type).toBe(SystemEventType.TEST_EXECUTED); + // ... more assertions +}); +``` + +#### Projection Tests + +These tests verify that events are correctly projected to read models: + +```typescript +// From src/infra/integration-tests/projection.integration.test.ts +test('TEST_EXECUTED command creates a record in system_status table', async () => { + // Create a unique test ID + const testId = uuidv4(); + const testName = 'integration-test'; + + // Create and dispatch a command + const cmd = { + id: uuidv4(), + tenant_id: tenantId, + type: SystemCommandType.EXECUTE_TEST, + payload: { + systemId: systemId, + testId, + testName, + }, + metadata: { + userId: testerId, + role: 'tester', + timestamp: new Date() + } + }; + + await dispatchCommand(cmd); + + // Verify the projection created a record + const result = await pool.query(sql` + SELECT * FROM system_status WHERE id = ${systemId} + `); + + expect(result.rows).toHaveLength(1); + const record = result.rows[0]; + expect(record.id).toBe(systemId); + expect(record.tenant_id).toBe(tenantId); + expect(record.testName).toBe(testName); + expect(record.result).toBe('success'); + expect(record.numberExecutedTests).toBe(1); +}); +``` + +This test verifies the entire flow from command dispatch to projection update, ensuring that the system works end-to-end. + +#### Multi-Tenancy Tests + +These tests verify tenant isolation: + +```typescript +// From src/infra/integration-tests/projection.integration.test.ts +// Verify tenant isolation +expect(result.rows.every((row: { tenant_id: any; }) => row.tenant_id === tenantId)).toBe(true); +expect(result.rows.every((row: { tenant_id: any; }) => row.tenant_id !== tenantId2)).toBe(true); +``` + +These tests ensure that data from one tenant is not visible to another tenant, which is critical for a multi-tenant system. + +#### Observability Tests + +These tests verify that the observability infrastructure works correctly: + +```typescript +// From src/infra/integration-tests/otel.test.ts +it('emits a projection.handle span', async () => { + memoryExporter.reset(); + + const evt: Event = { + id: randomUUID(), + type: 'testExecuted', + // ... other fields + }; + + await projectEvents([evt], pool); + + const spans = memoryExporter.getFinishedSpans(); + expect(spans.length).toBeGreaterThan(0); + expect(spans[0].name).toBe('projection.handle.testExecuted'); +}); +``` + +This test verifies that spans are created for observability, ensuring that the tracing infrastructure works correctly. + +## Testing Patterns + +Intent follows several key testing patterns to ensure effective and maintainable tests. + +### Arrange-Act-Assert + +Tests are structured using the Arrange-Act-Assert pattern: + +1. **Arrange**: Set up the test data and environment +2. **Act**: Perform the operation being tested +3. **Assert**: Verify the results + +This pattern makes tests clear and easy to understand. + +### Test Setup and Cleanup + +Tests use Jest's lifecycle hooks for setup and cleanup: + +```typescript +// From src/infra/integration-tests/snapshots.test.ts +beforeAll(async () => { + // Setup code + tenantId = process.env.TEST_TENANT_ID || 'test-tenant'; + // More setup +}, TEST_TIMEOUT); + +afterAll(async () => { + // Cleanup code + await pool.end(); +}); +``` + +This ensures that each test starts with a clean state and that resources are properly released after tests. + +### Test Data Generation + +Tests use helper functions and factories to generate test data: + +```typescript +// From src/core/base/__tests__/aggregate.test.ts +static createItemAddedEvent(aggregateId: UUID, item: string): Event { + return { + id: `event-${Math.random().toString(36).substring(2, 9)}`, + tenant_id: 'test-tenant', + type: ExampleEventType.ITEM_ADDED, + aggregateId, + aggregateType: 'example', + version: 1, + payload: { item } + }; +} +``` + +This makes tests more readable and reduces duplication. + +### Error Testing + +Tests verify that errors are thrown when expected: + +```typescript +// From src/core/system/__tests__/system.aggregate.test.ts +test('should throw error on simulate failure', () => { + const command = { + id: 'test-id', + tenant_id: 'test-tenant', + type: SystemCommandType.SIMULATE_FAILURE as const, + payload: {} as SimulateFailurePayload + }; + + expect(() => systemAggregate.handle(command)).toThrow('Simulated failure'); +}); +``` + +This ensures that the system handles error conditions correctly. + +## Testing Infrastructure + +Intent uses several tools and techniques to support testing. + +### Test Database + +Integration tests use a real PostgreSQL database, configured through environment variables: + +```typescript +// From src/infra/integration-tests/setup.ts +const pool = new Pool({ + host: process.env.TEST_DB_HOST || 'localhost', + port: parseInt(process.env.TEST_DB_PORT || '5432'), + user: process.env.TEST_DB_USER || 'postgres', + password: process.env.TEST_DB_PASSWORD || 'postgres', + database: process.env.TEST_DB_NAME || 'intent_test', +}); +``` + +This allows tests to verify actual database interactions. + +### In-Memory Adapters + +For faster unit tests, Intent provides in-memory implementations of key interfaces: + +```typescript +// Example of an in-memory event store for testing +export class InMemoryEventStore implements EventStorePort { + private events: Record = {}; + private snapshots: Record> = {}; + + async appendEvents(tenantId: UUID, events: Event[]): Promise { + // Implementation + } + + async loadEvents( + tenantId: UUID, + aggregateType: string, + aggregateId: UUID, + afterVersion?: number + ): Promise { + // Implementation + } + + // Other methods +} +``` + +These in-memory adapters allow tests to run quickly without external dependencies. + +### Test Utilities + +Intent includes various test utilities to simplify testing: + +```typescript +// From src/infra/observability/otel-test-tracer.ts +import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; + +export const memoryExporter = new InMemorySpanExporter(); + +const provider = new NodeTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(memoryExporter)], +}); + +provider.register({ contextManager }); +``` + +These utilities make it easier to write tests and verify system behavior. + +## Continuous Integration + +Intent uses CI pipelines to run tests automatically and ensure code quality. + +### CI Workflows + +The CI pipeline includes several workflows: + +1. **Unit Tests**: Run all unit tests +2. **Integration Tests**: Run all integration tests +3. **Core Linter**: Verify core domain consistency +4. **Projection Linter**: Verify projection access policies +5. **Drift Checker**: Verify projection schema consistency + +### Specialized Linters + +In addition to standard tests, Intent includes specialized linters: + +1. **Core Domain Linter**: Verifies that every command has a routing (aggregate) defined, all roles and commands are registered in the access model, etc. +2. **Projection RLS Policy Linter**: Checks that every read model (projection) has proper access control defined and that RLS policies cover all roles/columns as expected. +3. **Projection Drift Checker**: Detects schema drift between code and the database. + +These linters run in CI to catch issues early: + +```typescript +// Example of running the projection linter in CI +const result = await checkProjectionPolicies(pool); +if (result.errors.length > 0) { + console.error('Projection policy check failed:', result.errors); + process.exit(1); +} +``` + +## Best Practices for Writing Tests + +When adding new features to Intent, follow these testing best practices: + +1. **Write Unit Tests First**: Start with unit tests for new components +2. **Add Integration Tests**: Verify that the new components work with the rest of the system +3. **Test Edge Cases**: Include tests for error conditions and edge cases +4. **Verify Multi-Tenancy**: Ensure that tenant isolation is maintained +5. **Check Observability**: Verify that the new code is properly instrumented for observability +6. **Run Linters**: Use the specialized linters to catch issues early + +Example of a good test structure: + +```typescript +describe('MyNewFeature', () => { + // Setup code + + describe('GIVEN Command TYPE, When Payload {}', () => { + it('Expect outcome', () => { + // Expect your event or failure modes + }); + }); + + // Cleanup code +}); +``` + +## Benefits and Challenges + +### Benefits + +1. **Confidence**: Comprehensive tests provide confidence that the system works correctly +2. **Documentation**: Tests serve as living documentation of how the system should behave +3. **Regression Prevention**: Tests catch regressions before they reach production +4. **Refactoring Support**: Tests make it safer to refactor and evolve the codebase +5. **Quality Assurance**: CI integration ensures consistent quality + +### Challenges + +1. **Test Performance**: Integration tests can be slow due to database interactions +2. **Test Independence**: Ensuring tests don't interfere with each other +3. **Test Data Management**: Creating and cleaning up test data +4. **Environment Dependencies**: Managing test environment configuration +5. **Temporal Workflow Testing**: Testing long-running workflows can be challenging + +Intent addresses these challenges through isolation techniques, and specialized testing tools. + +## Conclusion + +Testing is a core part of Intent's development process, ensuring that the system remains reliable, maintainable, and evolvable. By following the testing patterns and practices described in this document, developers can contribute to Intent with confidence, knowing that their changes will be thoroughly tested and validated. \ No newline at end of file diff --git a/docs/basics/introduction.md b/docs/basics/introduction.md new file mode 100644 index 00000000..d48f24ff --- /dev/null +++ b/docs/basics/introduction.md @@ -0,0 +1,58 @@ +# Introduction to Intent + +Intent is a pragmatic, ports-first reference platform for multi-tenant, event-sourced CQRS back-ends powered by TypeScript and [Temporal](https://github.com/temporalio/temporal) for durable workflow execution. It turns event-sourcing theory into a platform you can demo in five minutes. + +## Who is it for? + +Intent is designed for developers building high-fidelity, multi-tenant backends with complex business logic. It's particularly well-suited for: + +- AI orchestration systems +- Financial applications +- Manufacturing industries +- Async-heavy workflows +- SaaS platforms +- High-complexity business domain applications + +Here's a comparison and perspective of Event Sourcing, the model behind Intent and traditional CRUD. + +## Core Design Principles + +Intent is built on several modern software design patterns: + +- **Domain-Driven Design (DDD)**: The codebase is organized around the business domain, with clear separation between core domain logic and infrastructure concerns. +- **Event Sourcing**: The system uses events as the source of truth, storing all changes to the application state as a sequence of events. +- **Command Query Responsibility Segregation (CQRS)**: Commands (write operations) and queries (read operations) are separated, with different models for writing and reading data. +- **Hexagonal Architecture**: Technology-agnostic core logic with adapters for infrastructure that plug in via explicit, testable ports. +- **Multi-tenancy**: The system is designed to support multiple tenants, with tenant isolation at various levels. + +## Key Capabilities + +| Capability | What it gives you | +|-------------------------------------|-------------------| +| **Lossless backends** | Guarantees no data loss, even under retries, crashes, or partial failures. Every command, event, and projection is persisted and replayable. | +| **Built-in multi-tenant isolation** | Tenant IDs propagate edge → core → infra. Row isolation in DB and namespaced workflows prevent accidental cross-tenant access or leaks. | +| **Automatic RLS enforcement** | Each projection declares access rules in metadata; they are compiled into Postgres RLS policies. CI linter blocks insecure access before it ships. | +| **Temporal workflow orchestration** | Commands and events are processed in durable Temporal workflows → supports back-pressure, retries, and exactly-once delivery at the source of truth. | +| **Observability** | Unified structured logging with context-aware `LoggerPort`, customizable log levels, and error serialization. OpenTelemetry spans wrap all key flows; logs and traces correlate via causation/correlation IDs. | + +## Key Terminology + +Intent uses several key concepts that are central to its architecture: + +- **Commands**: Represent an intent to change the system state. +- **Events**: Records of things that have happened in the system. +- **Aggregates**: Clusters of domain objects treated as a single unit for data changes. +- **Sagas**: Orchestrate complex business processes. +- **Projections**: Update read models based on events. + +These concepts will be covered in more depth in later sections of the documentation. + +## Why Intent? + +Intent is more than a framework. It's an event-sourced CQRS reference platform designed from first principles for simplicity and developer velocity in multi-tenant TypeScript backends. + +- **Reference Architecture**: Strict hexagonal boundaries, ports/adapters separation, and vertical slicing. Every workflow and projection is testable, composable, and observable. +- **Built for Safety and Evolution**: Automated RLS policy generation, drift detection and repair, snapshotting, and schema upcasting are not afterthoughts -- they are part of the platform's DNA. +- **Full-Stack Dev Experience**: The DevX companion UI, CLI flows, and local-first patterns make simulating, debugging, and evolving your event-sourced system immediate and visual. +- **Multi-Tenant and Policy-First**: Tenant isolation and access policies are enforced from edge to core to database -- by design, not convention. +- **Transparent, Documented, and Extensible**: Every architectural decision is captured in living ADRs. The codebase is structured for clarity, modification, and onboarding. \ No newline at end of file diff --git a/docs/basics/project-structure.md b/docs/basics/project-structure.md new file mode 100644 index 00000000..c2bd5a31 --- /dev/null +++ b/docs/basics/project-structure.md @@ -0,0 +1,113 @@ +# Project Structure and Architectural Layers + +This document explains how the Intent repository is organized and how the architecture is layered. + +## Repository Structure + +The Intent project follows a structured organization that reflects its architectural principles: + +``` +. +├── ADRs/ # Architectural decision logs +├── Dockerfile.worker # Container for Temporal worker +├── README.md # Quick-start & high-level overview +├── docker-compose.yml # Local infra: Postgres, Temporal, Supabase +├── docs/ # Documentation +├── jest*.config.js # Unit / integration test configs +├── setup.sh # One-shot project bootstrap helper +├── src/ # Source code +│ ├── core/ # Domain logic +│ ├── infra/ # Infrastructure adapters +│ ├── tools/ # Developer tools +│ ├── server.ts # Optional HTTP entry-point +│ └── worker.ts # Temporal worker bootstrap +└── temporal-config/ # Dynamic config for local Temporal +``` + +## Architectural Layers + +Intent follows a hexagonal (ports-and-adapters) architecture, which is reflected in its directory structure. The codebase is organized into three main layers: + +### 1. Core (Domain Layer) - `src/core/` + +The Core layer contains pure business logic, organized around the domain: + +- **Aggregates**: Domain entities that encapsulate business rules and state +- **Commands**: Instructions to change the system state +- **Events**: Records of state changes +- **Sagas**: Orchestrators for complex business processes + +Key characteristics: +- No dependency on infrastructure +- Replay-safe and testable +- Organized into domain-specific vertical slices (e.g., `system/`, `orders/`) + +### 2. Infra (Adapter Layer) - `src/infra/` + +The Infra layer provides concrete implementations of the ports defined in the Core layer: + +- **PostgreSQL adapters**: Event store and projection implementations +- **Temporal adapters**: Workflow engine integration +- **Authentication adapters**: User authentication and authorization + +Key characteristics: +- Adapters plug into the Core layer via explicit ports +- Respects domain boundaries (no core leakage) +- Handles cross-cutting concerns like multi-tenancy and security + +### 3. Tools Layer - `src/tools/` + +The Tools layer provides developer utilities and CI/CD helpers: + +- **Setup tools**: For initializing the event store, running migrations, etc. +- **Drift repair**: For detecting and fixing projection schema drift +- **Linting tools**: For enforcing RLS policies and other security measures +- **DevX helpers**: CLI and UI tools for developer experience + +Key characteristics: +- Tied into CI for consistency enforcement +- Provides automation for common development tasks +- Ensures security and correctness of the codebase + +## Multi-Tenancy and Security + +Multi-tenancy and security are cross-cutting concerns that span all layers of the architecture: + +- **Tenant ID**: Present in all commands, events, and database tables +- **Row-Level Security (RLS)**: Enforced at the database level to ensure tenant isolation +- **Access Control**: Built into projections via metadata that generates RLS policies + +## Key Directories in Detail + +### `src/core/` + +- `src/core/aggregates.ts`: Base aggregate class and registry +- `src/core/contracts.ts`: Core interfaces for commands, events, etc. +- `src/core/command-bus.ts`: Command routing and handling +- `src/core/event-bus.ts`: Event routing and handling +- `src/core/*/read-models/`: Projection definitions for each domain + +### `src/infra/` + +- `src/infra/pg/`: PostgreSQL event store and projection implementations +- `src/infra/temporal/`: Temporal workflow and activity implementations +- `src/infra/integration-tests/`: End-to-end tests for the system +- `src/infra/memory/`: In-memory implementations for testing +- `src/infra/observability/`: Tracing and logging implementations + +### `src/tools/` + +- `src/tools/setup/`: Interactive setup CLI +- `src/tools/projection-drift/`: Tools for detecting and fixing schema drift +- `src/tools/policy-linter/`: Tools for enforcing RLS policies + +## Hexagonal Architecture Benefits + +The hexagonal architecture provides several benefits: + +1. **Testability**: Core logic can be tested in isolation without infrastructure dependencies +2. **Flexibility**: Infrastructure implementations can be swapped without changing core logic +3. **Clarity**: Clear separation of concerns makes the codebase easier to understand +4. **Evolution**: The system can evolve over time without breaking existing functionality + +By maintaining this separation, Intent ensures that business logic remains pure and infrastructure concerns don't leak into the domain model. \ No newline at end of file diff --git a/docs/basics/quickstart.md b/docs/basics/quickstart.md new file mode 100644 index 00000000..db8f56cd --- /dev/null +++ b/docs/basics/quickstart.md @@ -0,0 +1,96 @@ +# Quickstart Guide + +This guide will help you get Intent running locally in about 5 minutes. + +## Prerequisites + +Before you begin, ensure you have the following tools installed: + +| Tool | Minimum Version | Notes | +|------|-----------------|-------| +| **Docker** | `24.x` | Engine + CLI; enables `docker compose` used by the Quick-start. | +| **Node.js** | `22.x` (current LTS) | TS/ESM project; lower versions are not tested. | +| **Git** | any modern release | Needed to clone the repo. | +| **Unix-like shell** | bash/zsh/fish | Commands assume a POSIX shell. Windows users can use WSL2 or Git Bash (untested). | + +## Step-by-Step Setup + +Follow these steps to get Intent running locally: + +1. **Clone the repository** + ```bash + git clone https://github.com/geeewhy/intent.git + cd intent + ``` + +2. **Start the required services using Docker** + ```bash + docker compose up -d postgres temporal temporal-ui + ``` + This starts PostgreSQL (for the event store) and Temporal (for workflow orchestration). + +3. **Set up the event store** + ```bash + npm run setup eventstore + ``` + This creates the necessary database schemas and seeds the Row-Level Security (RLS) policies. + +4. **Configure environment variables** + ```bash + cp .env.example .env + ``` + You can edit the `.env` file if you need to customize credentials or other settings. + +5. **Start the Temporal workers** + ```bash + npm run dev:worker aggregates # starts the aggregates worker + npm run dev:worker sagas # starts the sagas worker + ``` + These workers process commands and orchestrate workflows. + +## Verifying Your Setup + +After completing the steps above, you should have: + +- PostgreSQL running with the event store schema +- Temporal server and UI running +- Two Temporal workers processing commands and sagas + +You can verify that everything is running correctly by: + +- Checking Docker containers: `docker ps` should show postgres, temporal, and temporal-ui containers running +- Accessing the Temporal UI at http://localhost:8088 to see registered workflows + +## Interacting with the System + +To interact with your running Intent system, you can use the DevX UI companion: + +1. Navigate to the DevX UI directory: + ```bash + cd devex-ui + ``` + +2. Install dependencies and start the UI: + ```bash + npm install + npm run dev + ``` + +3. Open the UI in your browser (typically at http://localhost:8080), but will use different port if port is occupied. Follow what your console says. + +The DevX UI allows you to: +- Issue test commands +- Inspect events +- View traces +- Explore projections + +For more details on using the DevX UI, see the [DevX UI documentation](../devx/devx-ui.md). + +## Troubleshooting + +If you encounter issues: + +- Ensure all Docker containers are running: `docker ps` +- Check logs for the workers: `LOG_LEVEL=debug npm run dev:worker aggregates` +- Verify your `.env` file has the correct configuration +- Make sure ports 5432 (PostgreSQL) and 7233 (Temporal) are not in use by other applications \ No newline at end of file diff --git a/docs/devx/cli-tools.md b/docs/devx/cli-tools.md new file mode 100644 index 00000000..864395b6 --- /dev/null +++ b/docs/devx/cli-tools.md @@ -0,0 +1,167 @@ +# CLI Tools and Automation + +Intent includes a suite of command-line tools to streamline development, maintain consistency, and enforce best practices. These tools, located in `src/tools/`, help manage the system's schema, policies, and overall code quality. + +## Setup Tool + +The Setup Tool is an interactive CLI for setting up and configuring infrastructure components like the event store, database schemas, and RLS policies. + +### Key Features + +- Modular architecture with pluggable flows and providers +- Interactive and non-interactive modes +- Configurable steps for each flow +- Support for multiple providers (e.g., PostgreSQL) +- Environment variable management + +### Usage + +```bash +# Run a flow with default options +npm run setup -- + +# Run a flow with a specific provider +npm run setup -- --provider + +# Run a flow with a specific path +npm run setup -- --path + +# Run a flow with automatic confirmation (no prompts) +npm run setup -- --yes + +# Start interactive mode +npm run setup -- interactive +# or +npm run setup -- i +``` + +### Available Flows + +- **eventstore**: Setup and manage event store infrastructure + - `initial`: Fresh bootstrap of event store + - `upgrade`: Apply migrations on existing store + - `test`: Run tests on existing store + +Instead of manual SQL or console steps, developers can run `npm run setup eventstore` to perform initial project bootstrap or CI environment setup. + +## Projection Drift Check Tool + +The Projection Drift Check Tool detects schema drift between projection definitions in code and actual database tables. + +### What It Checks + +- Missing columns in the database +- Extra columns in the database +- Case or underscore inconsistencies between code and database + +### Usage + +```bash +# Run the drift check +npm run tool:projection-check-drift + +# Output the report to a file +npm run tool:projection-check-drift -- --out=drift-report.json +``` + +This tool scans the `ProjectionDefinition` metadata vs. the actual DB tables and warns if there's a mismatch, helping maintain consistency when projections evolve. + +## Projection Repair Tool + +The Projection Repair Tool builds on drift detection to automatically generate SQL migration scripts to fix schema differences. + +### Key Features + +- Rebuild tables from scratch +- Replay events to update existing tables +- Process only new events since the last checkpoint + +### Usage + +```bash +# Rebuild a specific table +npm run tool:projection-repair -- --table + +# Rebuild a specific projection +npm run tool:projection-repair -- --projection + +# Rebuild all projections +npm run tool:projection-repair -- --all + +# Rebuild only tables with issues from a drift report +npm run tool:projection-repair -- --drift-report + +# Resume mode: only replay events newer than current table state +npm run tool:projection-repair -- --resume --table +``` + +This tool can save time by avoiding manual SQL writing when a projection's shape changes. + +## Projection RLS Policy Linter + +The Projection RLS Policy Linter checks that every read model (projection) has proper access control defined and that RLS policies cover all roles/columns as expected. + +### What It Checks + +- Missing SQL enforcement functions +- Empty SQL strings +- Missing tenant isolation +- Condition name mismatches +- Duplicate conditions +- Unused scopes + +### Usage + +```bash +# Run linter in normal mode +npm run tool:projections-lint + +# Run linter in fix mode to generate patch-ready fixes +npm run tool:projections-lint -- --fix +``` + +This tool ensures no projection is left unprotected, maintaining security across the system. + +## Core Domain Linter + +The Core Domain Linter verifies core consistency across the domain model. + +### What It Checks + +1. **Command Payload Routing**: Ensures that all registered commands with a payload schema also declare proper aggregate routing (if not saga-only). + +2. **Role Consistency**: Extracts roles from condition functions and compares them against registered roles to identify any unregistered roles being used in access policies. + +### Usage + +```bash +npm run tool:core-lint +``` + +This tool catches misconfigurations in the domain code before they become problems at runtime. + +## Integration with CI Pipelines + +All these tools are integrated into CI pipelines to ensure consistency across the codebase. For example: + +```yaml +# Example GitHub Actions workflow steps +- name: Check projection drift + run: npm run tool:projection-check-drift + +- name: Lint RLS policies + run: npm run tool:projections-lint + +- name: Lint core domain + run: npm run tool:core-lint +``` + +Failing the linter or drift check will block a build, reinforcing their importance in maintaining system integrity. + +## When to Use These Tools + +- **Setup Tool**: When setting up a new environment or updating infrastructure +- **Projection Drift Check**: After modifying projection definitions to ensure DB consistency +- **Projection Repair**: When drift is detected and needs to be fixed +- **RLS Policy Linter**: After adding or modifying read models to ensure proper access control +- **Core Domain Linter**: After adding new commands or roles to ensure proper configuration diff --git a/docs/devx/devx-ui.md b/docs/devx/devx-ui.md new file mode 100644 index 00000000..9bea4844 --- /dev/null +++ b/docs/devx/devx-ui.md @@ -0,0 +1,94 @@ +# Developer Companion UI (DevX UI) + +The Intent DevX UI is a web-based side panel that lets you interact with the running Intent system in real time. It serves as a companion tool for developers, providing a visual interface to work with the event-sourced system during development and debugging. + +## Current Features + +The DevX UI offers several powerful features to enhance the developer experience: + +* **Command issuer** – A form-driven interface to create and dispatch commands, with schema validation. This allows you to interact with the system without writing code. + +* **Event stream viewer** – Live tail of events as they occur, with filters. Watch events flow through the system in real time as you issue commands. + +* **Trace viewer** – Visualization of correlated spans/traces to follow a command through to completion. See how a command propagates through the system, from initial handling to event generation and projection updates. + +* **Projections explorer** – View the state of read models and detect schema drift or issues. Inspect the current state of your projections and ensure they're in sync with your code. + +* **AI assistant (experimental)** – Scaffolding for commands/sagas and Q&A with context. Get help generating new commands or understanding the system. + +* **Multi-tenant/role switching** – Simulate different user perspectives by switching between tenants and roles. Test how your system behaves for different users. + +## Running the DevX UI Locally + +To run the DevX UI locally, follow these steps: + +1. Run the admin API +```bash + npm run api:admin +``` + +2. Navigate to the DevX UI directory in another terminal: + ```bash + cd devex-ui/ + ``` + +3. Install dependencies: + ```bash + npm install + ``` + +4. Start the development server: + ```bash + npm run dev + ``` + +5. Open the given local URL in your browser. + +## Configuration Options + +By default, the DevX UI runs in mock mode with fake data, which is useful for quick experimentation without a running backend. To connect to a real Intent instance: + +1. Set the API mode to 'real' in your environment: + ``` + VITE_API_MODE='real' + ``` + +2. Point to your running backend API: + ``` + VITE_API_URL='http://localhost:3000' + ``` + +You can set these variables in a `.env.local` file in the `devex-ui/` directory or pass them when starting the development server: + +```bash +VITE_API_MODE='real' VITE_API_URL='http://localhost:3000' npm run dev +``` + +## Architecture and Safety + +The DevX UI is designed to be both powerful and safe: + +- It uses the same backend ports and respects access control, so using the UI is like using the real system, not bypassing it. +- It's "local-first", making it useful both for quick experimentation and real debugging. +- The UI is completely separate from the core system, so it can't compromise the integrity of your production environment. + +## When to Use the DevX UI + +The DevX UI is particularly useful in the following scenarios: + +- **During development**: Quickly fire commands instead of writing manual scripts to test your features. +- **Debugging**: Watch how events flow when testing a new feature and identify issues in the event chain. +- **Learning the system**: Explore the command and event structure to understand how the system works. +- **Demonstrating functionality**: Show stakeholders how the system behaves without writing code. +- **Testing multi-tenant scenarios**: Switch between tenants to verify proper isolation and behavior. + +## Getting Started + +After launching the DevX UI, you'll see a dashboard with different panels for each feature. Here's a quick guide to get started: + +1. Use the **Command Issuer** panel to create and send a test command +2. Watch the **Event Stream** panel to see the events generated by your command +3. Check the **Trace Viewer** to see how your command was processed +4. Explore the **Projections** panel to see how the read models were updated + +This workflow gives you a complete view of how commands flow through the system, from initial dispatch to final state changes. \ No newline at end of file