diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..03ab097 --- /dev/null +++ b/.env.example @@ -0,0 +1,44 @@ +# Ingress +TIPS_INGRESS_ADDRESS=127.0.0.1 +TIPS_INGRESS_PORT=8080 +TIPS_INGRESS_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres +TIPS_INGRESS_RPC_MEMPOOL=http://localhost:2222 +TIPS_INGRESS_DUAL_WRITE_MEMPOOL=false +TIPS_INGRESS_KAFKA_BROKERS=localhost:9092 +TIPS_INGRESS_KAFKA_TOPIC=tips-audit +TIPS_INGRESS_LOG_LEVEL=info + +# Maintenance +TIPS_MAINTENANCE_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres +TIPS_MAINTENANCE_RPC_NODE=http://localhost:2222 +TIPS_MAINTENANCE_KAFKA_BROKERS=localhost:9092 +TIPS_MAINTENANCE_KAFKA_TOPIC=tips-audit +TIPS_MAINTENANCE_POLL_INTERVAL_MS=250 +TIPS_MAINTENANCE_LOG_LEVEL=info + +# Audit service configuration +TIPS_AUDIT_KAFKA_BROKERS=localhost:9092 +TIPS_AUDIT_KAFKA_TOPIC=tips-audit +TIPS_AUDIT_KAFKA_GROUP_ID=local-audit +TIPS_AUDIT_LOG_LEVEL=info +TIPS_AUDIT_S3_BUCKET=tips +TIPS_AUDIT_S3_CONFIG_TYPE=manual +TIPS_AUDIT_S3_ENDPOINT=http://localhost:7000 +TIPS_AUDIT_S3_REGION=us-east-1 +TIPS_AUDIT_S3_ACCESS_KEY_ID=minioadmin +TIPS_AUDIT_S3_SECRET_ACCESS_KEY=minioadmin + +# For production AWS (uncomment and remove manual config above) +# TIPS_AUDIT_S3_CONFIG_TYPE=aws + +# TIPS UI +TIPS_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres +TIPS_UI_AWS_REGION=us-east-1 +TIPS_UI_S3_BUCKET_NAME=tips +TIPS_UI_S3_CONFIG_TYPE=manual +TIPS_UI_S3_ENDPOINT=http://localhost:7000 +TIPS_UI_S3_ACCESS_KEY_ID=minioadmin +TIPS_UI_S3_SECRET_ACCESS_KEY=minioadmin + +# For production AWS (uncomment and remove manual config above) +# TIPS_UI_S3_CONFIG_TYPE=aws \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7d84b0d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [ main, prototype ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - run: cargo clippy -- -D warnings + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo build --release \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c68c9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Rust +/target/ +Cargo.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment variables +.env +.env.local + +# Backup files +*~ +*.bak + +# Next.js (ui/) +ui/.next/ +ui/out/ +ui/node_modules/ +ui/.env*.local +ui/.vercel +ui/*.tsbuildinfo +ui/next-env.d.ts + +# Local data directories +data/ diff --git a/.sqlx/query-ca6a250821d4542720578da20aa9cf31e808fa8dcbd701c75246dbdc95c58946.json b/.sqlx/query-ca6a250821d4542720578da20aa9cf31e808fa8dcbd701c75246dbdc95c58946.json new file mode 100644 index 0000000..ad77700 --- /dev/null +++ b/.sqlx/query-ca6a250821d4542720578da20aa9cf31e808fa8dcbd701c75246dbdc95c58946.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO bundles (\n id, senders, minimum_base_fee, txn_hashes, \n txs, reverting_tx_hashes, dropping_tx_hashes, \n block_number, min_timestamp, max_timestamp,\n created_at, updated_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "BpcharArray", + "Int8", + "BpcharArray", + "TextArray", + "BpcharArray", + "BpcharArray", + "Int8", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "ca6a250821d4542720578da20aa9cf31e808fa8dcbd701c75246dbdc95c58946" +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca1f6c3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# Project: Tips - Transaction Inclusion Pipeline Services + +## Notes +- DO NOT ADD COMMENTS UNLESS INSTRUCTED +- Put imports at the top of the file, never in functions +- For rust tasks always run `just rust-ci` before claiming a task is complete and fix any issues +- For typescript tasks always run `just ui-ci` before claiming a task is complete and fix any issues +- Use `just fix` to fix formatting and warnings +- Always add dependencies to the cargo.toml in the root and reference them in the crate cargo files +- Always use the latest dependency versions. Use https://crates.io/ to find dependency versions when adding new deps + +## Available CI Commands +- `just ci` - Run all CI checks (both Rust and UI) +- `just rust-ci` - Run Rust-specific CI checks (check, test, fmt, clippy, build) +- `just ui-ci` - Run UI-specific CI checks + +## Project Structure +``` +├── Cargo.toml # Workspace configuration +├── ingress/ # Main binary crate +├── .github/workflows/ +│ └── ci.yml # GitHub Actions CI +``` diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0827cf1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,62 @@ +[workspace] +members = ["crates/ingress", "crates/datastore", "crates/audit", "crates/maintenance"] +resolver = "2" + +[workspace.dependencies] +tips-ingress = { path = "crates/ingress" } +tips-datastore = { path = "crates/datastore" } +tips-audit = { path = "crates/audit" } +tips-maintenance = { path = "crates/maintenance" } + +jsonrpsee = { version = "0.26.0", features = ["server", "macros"] } + +# alloy +alloy-primitives = { version = "1.3.1", default-features = false, features = [ + "map-foldhash", + "serde", +] } +alloy-rpc-types = { version = "1.0.32", default-features = false } +alloy-consensus = { version = "1.0.32" } +alloy-provider = { version = "1.0.32" } +alloy-rpc-client = { version = "1.0.32" } +alloy-rpc-types-mev = "1.0.32" +alloy-transport-http = "1.0.32" +alloy-rlp = "0.3.12" + +# op-alloy +op-alloy-rpc-types = { version = "0.20.0" } +op-alloy-consensus = { version = "0.20.0", features = ["k256"] } +op-alloy-network = {version = "0.20.0"} + +tokio = { version = "1.47.1", features = ["full"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } +anyhow = "1.0.99" +clap = { version = "4.5.47", features = ["derive", "env"] } +url = "2.5.7" +sqlx = { version = "0.8.6", features = [ + "runtime-tokio-native-tls", + "postgres", + "uuid", + "chrono", + "json", +]} +uuid = { version = "1.18.1", features = ["v4", "serde"] } +serde = { version = "1.0.219", features = ["derive"] } +chrono = { version = "0.4.42", features = ["serde"] } +eyre = "0.6.12" +async-trait = "0.1.89" +serde_json = "1.0.143" +dotenvy = "0.15.7" +testcontainers = { version = "0.23.1", features = ["blocking"] } +testcontainers-modules = { version = "0.11.2", features = ["postgres", "kafka", "minio"] } +futures-util = "0.3.32" + +# Kafka and S3 dependencies +rdkafka = { version = "0.37.0", features = ["libz-static"] } +aws-config = "1.1.7" +aws-sdk-s3 = "1.106.0" +aws-credential-types = "1.1.7" +bytes = { version = "1.8.0", features = ["serde"] } +md5 = "0.7.0" +base64 = "0.22.1" \ No newline at end of file diff --git a/README.md b/README.md index 985bb4d..ee78e8a 100644 --- a/README.md +++ b/README.md @@ -1 +1,81 @@ # tips +A prototype of a Transaction Inclusion & Prioritization Stack for private sequencers that does not use the P2P mempool. The project +aims to increase throughput, improve transaction tracking, reduce latency and add support for bundles. + +This project is currently at: + +https://github.com/flashbots/builder-playground +https://github.com/base/tips/pull/new/prototype +https://github.com/base/op-rbuilder/pull/new/tips-prototype + +## Architecture Overview + +The project consists of three main crates: + +### 🔌 Ingress (`crates/ingress`) +The main entry point that provides a JSON-RPC API for receiving transactions and bundles. It handles: +- Transaction validation and processing +- Bundle creation and management +- Dual-write capability to legacy mempool +- PostgreSQL persistence via the datastore layer + +### 🗄️ Datastore (`crates/datastore`) +PostgreSQL-based storage layer that provides: +- Bundle persistence and retrieval +- Transaction tracking and indexing +- Database schema management via migrations +- See [Database Schema](crates/datastore/migrations/1757444171_create_bundles_table.sql) for table structure + +### 📊 Audit (`crates/audit`) +Event streaming and archival system that: +- Publishes bundle lifecycle events to Kafka +- Archives bundle history to S3 for long-term storage +- Provides transaction lookup capabilities +- See [S3 Storage Format](crates/audit/S3_FORMAT.md) for data structure details + + +### Local Development +You can run the whole system locally with: + +tips: +```sh +just db && sleep 3 && just ingress +``` + +builder-playground: +```sh +# TODO: Figure out the flashblocks/websocket proxy/validator setup +go run main.go cook opstack --external-builder http://host.docker.internal:4444 --enable-latest-fork 0 +``` + +op-rbuilder: +```sh +just run-playground + +# Send transactions with +just send-txn +``` + +[optional] tips +```sh +just ui +``` + +## Dev Notes + +### Services Access +- **MinIO UI**: http://localhost:7001 (minioadmin/minioadmin) +- **PostgreSQL**: localhost:5432 (postgres/postgres) +- **Kafka**: localhost:9092 + +### Debugging +```sh +# Connect to the database +psql -d postgres://postgres:postgres@localhost:5432/postgres + +# Update the UI's schema +just ui-db-schema + +# Start all services with Docker Compose +docker-compose up -d +``` \ No newline at end of file diff --git a/crates/audit/Cargo.toml b/crates/audit/Cargo.toml new file mode 100644 index 0000000..6d8168e --- /dev/null +++ b/crates/audit/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "tips-audit" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "tips-audit" +path = "src/bin/main.rs" + +[dependencies] +# Workspace dependencies +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +uuid = { workspace = true } +async-trait = { workspace = true } +alloy-primitives = { workspace = true } +alloy-consensus = { workspace = true } +alloy-provider = { workspace = true } +alloy-rpc-types-mev = { workspace = true } +op-alloy-consensus = { workspace = true } +clap = { workspace = true } +dotenvy = { workspace = true } + +# Additional dependencies for Kafka and S3 +rdkafka = { workspace = true } +aws-config = { workspace = true } +aws-sdk-s3 = { workspace = true } +aws-credential-types = { workspace = true } +bytes = { workspace = true } +md5 = { workspace = true } +futures = "0.3.31" + +[dev-dependencies] +tokio-test = "0.4.4" +testcontainers = { workspace = true } +testcontainers-modules = { workspace = true } +eyre = { workspace = true } \ No newline at end of file diff --git a/crates/audit/S3_FORMAT.md b/crates/audit/S3_FORMAT.md new file mode 100644 index 0000000..e64ffd6 --- /dev/null +++ b/crates/audit/S3_FORMAT.md @@ -0,0 +1,87 @@ +# S3 Storage Format + +This document describes the S3 storage format used by the audit system for archiving bundle lifecycle events and transaction lookups. + +## Storage Structure + +### Bundle History: `/bundles/` + +Each bundle is stored as a JSON object containing its complete lifecycle history: + +```json +{ + "history": [ + { + "event": "Created", + "timestamp": 1234567890, + "bundle": { + // EthSendBundle object + } + }, + { + "event": "Updated", + "timestamp": 1234567891, + "bundle": { + // EthSendBundle object + } + }, + { + "event": "Cancelled", + "timestamp": 1234567892 + }, + { + "event": "BuilderIncluded", + "builder": "builder-id", + "timestamp": 1234567893, + "blockNumber": 12345, + "flashblockIndex": 1 + }, + { + "event": "FlashblockIncluded", + "timestamp": 1234567894, + "blockNumber": 12345, + "flashblockIndex": 1 + }, + { + "event": "BlockIncluded", + "timestamp": 1234567895, + "blockNumber": 12345, + "blockHash": "0x..." + }, + { + "event": "Dropped", + "timestamp": 1234567896, + "reason": "TIMEOUT" + } + ] +} +``` + +### Transaction Lookup: `/transactions/by_hash/` + +Transaction hash to bundle mapping for efficient lookups: + +```json +{ + "bundle_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + ] +} +``` + +## Event Types + +### Bundle Events + +- **Created**: Initial bundle creation with transaction list +- **Updated**: Bundle modification (transaction additions/removals) +- **Cancelled**: Bundle explicitly cancelled +- **BuilderIncluded**: Bundle included by builder in flashblock +- **FlashblockIncluded**: Flashblock containing bundle included in chain +- **BlockIncluded**: Final confirmation in blockchain +- **Dropped**: Bundle dropped from processing + +### Drop Reasons + +- `TIMEOUT`: Bundle expired without inclusion \ No newline at end of file diff --git a/crates/audit/src/archiver.rs b/crates/audit/src/archiver.rs new file mode 100644 index 0000000..7bbb4ec --- /dev/null +++ b/crates/audit/src/archiver.rs @@ -0,0 +1,51 @@ +use crate::reader::MempoolEventReader; +use crate::storage::MempoolEventWriter; +use anyhow::Result; +use std::time::Duration; +use tokio::time::sleep; +use tracing::{error, info}; + +pub struct KafkaMempoolArchiver +where + R: MempoolEventReader, + W: MempoolEventWriter, +{ + reader: R, + writer: W, +} + +impl KafkaMempoolArchiver +where + R: MempoolEventReader, + W: MempoolEventWriter, +{ + pub fn new(reader: R, writer: W) -> Self { + Self { reader, writer } + } + + pub async fn run(&mut self) -> Result<()> { + info!("Starting Kafka mempool archiver"); + + loop { + match self.reader.read_event().await { + Ok(event) => { + if let Err(e) = self.writer.archive_event(event).await { + error!( + error = %e, + "Failed to write event" + ); + } else if let Err(e) = self.reader.commit().await { + error!( + error = %e, + "Failed to commit message" + ); + } + } + Err(e) => { + error!(error = %e, "Error reading events"); + sleep(Duration::from_secs(1)).await; + } + } + } + } +} diff --git a/crates/audit/src/bin/main.rs b/crates/audit/src/bin/main.rs new file mode 100644 index 0000000..b6a0b21 --- /dev/null +++ b/crates/audit/src/bin/main.rs @@ -0,0 +1,136 @@ +use anyhow::Result; +use aws_config::{BehaviorVersion, Region}; +use aws_credential_types::Credentials; +use aws_sdk_s3::{config::Builder as S3ConfigBuilder, Client as S3Client}; +use clap::{Parser, ValueEnum}; +use rdkafka::consumer::Consumer; +use tips_audit::{ + create_kafka_consumer, KafkaMempoolArchiver, KafkaMempoolReader, S3MempoolEventReaderWriter, +}; +use tracing::{info, warn}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Debug, Clone, ValueEnum)] +enum S3ConfigType { + Aws, + Manual, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(long, env = "TIPS_AUDIT_KAFKA_BROKERS")] + kafka_brokers: String, + + #[arg(long, env = "TIPS_AUDIT_KAFKA_TOPIC")] + kafka_topic: String, + + #[arg(long, env = "TIPS_AUDIT_KAFKA_GROUP_ID")] + kafka_group_id: String, + + #[arg(long, env = "TIPS_AUDIT_S3_BUCKET")] + s3_bucket: String, + + #[arg(long, env = "TIPS_AUDIT_LOG_LEVEL", default_value = "info")] + log_level: String, + + #[arg(long, env = "TIPS_AUDIT_S3_CONFIG_TYPE", default_value = "aws")] + s3_config_type: S3ConfigType, + + #[arg(long, env = "TIPS_AUDIT_S3_ENDPOINT")] + s3_endpoint: Option, + + #[arg(long, env = "TIPS_AUDIT_S3_REGION", default_value = "us-east-1")] + s3_region: String, + + #[arg(long, env = "TIPS_AUDIT_S3_ACCESS_KEY_ID")] + s3_access_key_id: Option, + + #[arg(long, env = "TIPS_AUDIT_S3_SECRET_ACCESS_KEY")] + s3_secret_access_key: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + + let args = Args::parse(); + + let log_level = match args.log_level.to_lowercase().as_str() { + "trace" => tracing::Level::TRACE, + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => { + warn!( + "Invalid log level '{}', defaulting to 'info'", + args.log_level + ); + tracing::Level::INFO + } + }; + + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level.to_string())), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + info!( + kafka_brokers = %args.kafka_brokers, + kafka_topic = %args.kafka_topic, + kafka_group_id = %args.kafka_group_id, + s3_bucket = %args.s3_bucket, + "Starting audit archiver" + ); + + let consumer = create_kafka_consumer(&args.kafka_brokers, &args.kafka_group_id)?; + consumer.subscribe(&[&args.kafka_topic])?; + + let reader = KafkaMempoolReader::new(consumer, args.kafka_topic.clone())?; + + let s3_client = create_s3_client(&args).await?; + let s3_bucket = args.s3_bucket.clone(); + let writer = S3MempoolEventReaderWriter::new(s3_client, s3_bucket); + + let mut archiver = KafkaMempoolArchiver::new(reader, writer); + + info!("Audit archiver initialized, starting main loop"); + + archiver.run().await +} + +async fn create_s3_client(args: &Args) -> Result { + match args.s3_config_type { + S3ConfigType::Manual => { + let region = args.s3_region.clone(); + let mut config_builder = + aws_config::defaults(BehaviorVersion::latest()).region(Region::new(region)); + + if let Some(endpoint) = &args.s3_endpoint { + config_builder = config_builder.endpoint_url(endpoint); + } + + if let (Some(access_key), Some(secret_key)) = + (&args.s3_access_key_id, &args.s3_secret_access_key) + { + let credentials = Credentials::new(access_key, secret_key, None, None, "manual"); + config_builder = config_builder.credentials_provider(credentials); + } + + let config = config_builder.load().await; + let s3_config_builder = S3ConfigBuilder::from(&config).force_path_style(true); + + info!(message = "manually configuring s3 client"); + Ok(S3Client::from_conf(s3_config_builder.build())) + } + S3ConfigType::Aws => { + info!(message = "using aws s3 client"); + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + Ok(S3Client::new(&config)) + } + } +} diff --git a/crates/audit/src/lib.rs b/crates/audit/src/lib.rs new file mode 100644 index 0000000..cfa9c53 --- /dev/null +++ b/crates/audit/src/lib.rs @@ -0,0 +1,11 @@ +pub mod archiver; +pub mod publisher; +pub mod reader; +pub mod storage; +pub mod types; + +pub use archiver::*; +pub use publisher::*; +pub use reader::*; +pub use storage::*; +pub use types::*; diff --git a/crates/audit/src/publisher.rs b/crates/audit/src/publisher.rs new file mode 100644 index 0000000..f72c1f8 --- /dev/null +++ b/crates/audit/src/publisher.rs @@ -0,0 +1,64 @@ +use crate::types::MempoolEvent; +use anyhow::Result; +use async_trait::async_trait; +use rdkafka::producer::{FutureProducer, FutureRecord}; +use serde_json; +use tracing::{debug, error}; +use uuid::Uuid; + +#[async_trait] +pub trait MempoolEventPublisher: Send + Sync { + async fn publish(&self, event: MempoolEvent) -> Result<()>; +} + +#[derive(Clone)] +pub struct KafkaMempoolEventPublisher { + producer: FutureProducer, + topic: String, +} + +impl KafkaMempoolEventPublisher { + pub fn new(producer: FutureProducer, topic: String) -> Self { + Self { producer, topic } + } + + async fn send_event(&self, event: &MempoolEvent) -> Result<()> { + let bundle_id = event.bundle_id(); + let key = format!("{}-{}", bundle_id, Uuid::new_v4()); + let payload = serde_json::to_vec(event)?; + + let record = FutureRecord::to(&self.topic).key(&key).payload(&payload); + + match self + .producer + .send(record, tokio::time::Duration::from_secs(5)) + .await + { + Ok(_) => { + debug!( + bundle_id = %bundle_id, + topic = %self.topic, + payload_size = payload.len(), + "Successfully published event" + ); + Ok(()) + } + Err((err, _)) => { + error!( + bundle_id = %bundle_id, + topic = %self.topic, + error = %err, + "Failed to publish event" + ); + Err(anyhow::anyhow!("Failed to publish event: {}", err)) + } + } + } +} + +#[async_trait] +impl MempoolEventPublisher for KafkaMempoolEventPublisher { + async fn publish(&self, event: MempoolEvent) -> Result<()> { + self.send_event(&event).await + } +} diff --git a/crates/audit/src/reader.rs b/crates/audit/src/reader.rs new file mode 100644 index 0000000..7d0ab13 --- /dev/null +++ b/crates/audit/src/reader.rs @@ -0,0 +1,138 @@ +use crate::types::MempoolEvent; +use anyhow::Result; +use async_trait::async_trait; +use rdkafka::{ + config::ClientConfig, + consumer::{Consumer, StreamConsumer}, + message::Message, + Timestamp, TopicPartitionList, +}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::time::sleep; +use tracing::{debug, error}; + +pub fn create_kafka_consumer(kafka_brokers: &str, group_id: &str) -> Result { + let consumer: StreamConsumer = ClientConfig::new() + .set("group.id", group_id) + .set("bootstrap.servers", kafka_brokers) + .set("enable.partition.eof", "false") + .set("session.timeout.ms", "6000") + .set("enable.auto.commit", "false") + .set("auto.offset.reset", "earliest") + .set("fetch.wait.max.ms", "100") + .set("fetch.min.bytes", "1") + .create()?; + Ok(consumer) +} + +pub fn assign_topic_partition(consumer: &StreamConsumer, topic: &str) -> Result<()> { + let mut tpl = TopicPartitionList::new(); + tpl.add_partition(topic, 0); + consumer.assign(&tpl)?; + Ok(()) +} + +#[derive(Debug, Clone)] +pub struct Event { + pub key: String, + pub event: MempoolEvent, + pub timestamp: i64, +} + +#[async_trait] +pub trait MempoolEventReader { + async fn read_event(&mut self) -> Result; + async fn commit(&mut self) -> Result<()>; +} + +pub struct KafkaMempoolReader { + consumer: StreamConsumer, + topic: String, + last_message_offset: Option, + last_message_partition: Option, +} + +impl KafkaMempoolReader { + pub fn new(consumer: StreamConsumer, topic: String) -> Result { + consumer.subscribe(&[&topic])?; + Ok(Self { + consumer, + topic, + last_message_offset: None, + last_message_partition: None, + }) + } +} + +#[async_trait] +impl MempoolEventReader for KafkaMempoolReader { + async fn read_event(&mut self) -> Result { + match self.consumer.recv().await { + Ok(message) => { + let payload = message + .payload() + .ok_or_else(|| anyhow::anyhow!("Message has no payload"))?; + + // Extract Kafka timestamp, use current time as fallback + let timestamp = match message.timestamp() { + Timestamp::CreateTime(millis) => millis, + Timestamp::LogAppendTime(millis) => millis, + Timestamp::NotAvailable => SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64, + }; + + let event: MempoolEvent = serde_json::from_slice(payload)?; + + debug!( + bundle_id = %event.bundle_id(), + timestamp = timestamp, + offset = message.offset(), + partition = message.partition(), + "Received event with timestamp" + ); + + self.last_message_offset = Some(message.offset()); + self.last_message_partition = Some(message.partition()); + + let key = message + .key() + .map(|k| String::from_utf8_lossy(k).to_string()) + .ok_or_else(|| anyhow::anyhow!("Message missing required key"))?; + + let event_result = Event { + key, + event, + timestamp, + }; + + Ok(event_result) + } + Err(e) => { + println!("received error"); + error!(error = %e, "Error receiving message from Kafka"); + sleep(Duration::from_secs(1)).await; + Err(e.into()) + } + } + } + + async fn commit(&mut self) -> Result<()> { + if let (Some(offset), Some(partition)) = + (self.last_message_offset, self.last_message_partition) + { + let mut tpl = TopicPartitionList::new(); + tpl.add_partition_offset(&self.topic, partition, rdkafka::Offset::Offset(offset + 1))?; + self.consumer + .commit(&tpl, rdkafka::consumer::CommitMode::Async)?; + } + Ok(()) + } +} + +impl KafkaMempoolReader { + pub fn topic(&self) -> &str { + &self.topic + } +} diff --git a/crates/audit/src/storage.rs b/crates/audit/src/storage.rs new file mode 100644 index 0000000..62501bd --- /dev/null +++ b/crates/audit/src/storage.rs @@ -0,0 +1,577 @@ +use crate::reader::Event; +use crate::types::{BundleId, DropReason, MempoolEvent, TransactionId}; +use alloy_primitives::TxHash; +use alloy_rpc_types_mev::EthSendBundle; +use anyhow::Result; +use async_trait::async_trait; +use aws_sdk_s3::error::SdkError; +use aws_sdk_s3::operation::get_object::GetObjectError; +use aws_sdk_s3::primitives::ByteStream; +use aws_sdk_s3::Client as S3Client; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::Debug; +use tracing::info; + +#[derive(Debug)] +pub enum S3Key { + Bundle(BundleId), + TransactionByHash(TxHash), +} + +impl fmt::Display for S3Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + S3Key::Bundle(bundle_id) => write!(f, "bundles/{}", bundle_id), + S3Key::TransactionByHash(hash) => write!(f, "transactions/by_hash/{}", hash), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TransactionMetadata { + pub bundle_ids: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum BundleHistoryEvent { + Created { + key: String, + timestamp: i64, + bundle: EthSendBundle, + }, + Updated { + key: String, + timestamp: i64, + bundle: EthSendBundle, + }, + Cancelled { + key: String, + timestamp: i64, + }, + BuilderIncluded { + key: String, + timestamp: i64, + builder: String, + block_number: u64, + flashblock_index: u64, + }, + FlashblockIncluded { + key: String, + timestamp: i64, + block_number: u64, + flashblock_index: u64, + }, + BlockIncluded { + key: String, + timestamp: i64, + block_number: u64, + block_hash: TxHash, + }, + Dropped { + key: String, + timestamp: i64, + reason: DropReason, + }, +} + +impl BundleHistoryEvent { + pub fn key(&self) -> &str { + match self { + BundleHistoryEvent::Created { key, .. } => key, + BundleHistoryEvent::Updated { key, .. } => key, + BundleHistoryEvent::Cancelled { key, .. } => key, + BundleHistoryEvent::BuilderIncluded { key, .. } => key, + BundleHistoryEvent::FlashblockIncluded { key, .. } => key, + BundleHistoryEvent::BlockIncluded { key, .. } => key, + BundleHistoryEvent::Dropped { key, .. } => key, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BundleHistory { + pub history: Vec, +} + +fn update_bundle_history_transform( + bundle_history: BundleHistory, + event: &Event, +) -> Option { + let mut history = bundle_history.history; + let bundle_id = event.event.bundle_id(); + + // Check for deduplication - if event with same key already exists, skip + if history.iter().any(|h| h.key() == event.key) { + info!( + bundle_id = %bundle_id, + event_key = %event.key, + "Event already exists, skipping due to deduplication" + ); + return None; + } + + let history_event = match &event.event { + MempoolEvent::Created { bundle, .. } => BundleHistoryEvent::Created { + key: event.key.clone(), + timestamp: event.timestamp, + bundle: bundle.clone(), + }, + MempoolEvent::Updated { bundle, .. } => BundleHistoryEvent::Updated { + key: event.key.clone(), + timestamp: event.timestamp, + bundle: bundle.clone(), + }, + MempoolEvent::Cancelled { .. } => BundleHistoryEvent::Cancelled { + key: event.key.clone(), + timestamp: event.timestamp, + }, + MempoolEvent::BuilderIncluded { + builder, + block_number, + flashblock_index, + .. + } => BundleHistoryEvent::BuilderIncluded { + key: event.key.clone(), + timestamp: event.timestamp, + builder: builder.clone(), + block_number: *block_number, + flashblock_index: *flashblock_index, + }, + MempoolEvent::FlashblockIncluded { + block_number, + flashblock_index, + .. + } => BundleHistoryEvent::FlashblockIncluded { + key: event.key.clone(), + timestamp: event.timestamp, + block_number: *block_number, + flashblock_index: *flashblock_index, + }, + MempoolEvent::BlockIncluded { + block_number, + block_hash, + .. + } => BundleHistoryEvent::BlockIncluded { + key: event.key.clone(), + timestamp: event.timestamp, + block_number: *block_number, + block_hash: *block_hash, + }, + MempoolEvent::Dropped { reason, .. } => BundleHistoryEvent::Dropped { + key: event.key.clone(), + timestamp: event.timestamp, + reason: reason.clone(), + }, + }; + + history.push(history_event); + let bundle_history = BundleHistory { history }; + + info!( + bundle_id = %bundle_id, + event_count = bundle_history.history.len(), + "Updated bundle history" + ); + + Some(bundle_history) +} + +fn update_transaction_metadata_transform( + transaction_metadata: TransactionMetadata, + bundle_id: BundleId, +) -> Option { + let mut bundle_ids = transaction_metadata.bundle_ids; + + if bundle_ids.contains(&bundle_id) { + return None; + } + + bundle_ids.push(bundle_id); + Some(TransactionMetadata { bundle_ids }) +} + +#[async_trait] +pub trait MempoolEventWriter { + async fn archive_event(&self, event: Event) -> Result<()>; +} + +#[async_trait] +pub trait MempoolEventS3Reader { + async fn get_bundle_history(&self, bundle_id: BundleId) -> Result>; + async fn get_transaction_metadata( + &self, + tx_hash: TxHash, + ) -> Result>; +} + +#[derive(Clone)] +pub struct S3MempoolEventReaderWriter { + s3_client: S3Client, + bucket: String, +} + +impl S3MempoolEventReaderWriter { + pub fn new(s3_client: S3Client, bucket: String) -> Self { + Self { s3_client, bucket } + } + + async fn update_bundle_history(&self, event: Event) -> Result<()> { + let s3_key = S3Key::Bundle(event.event.bundle_id()).to_string(); + + self.idempotent_write::(&s3_key, |current_history| { + update_bundle_history_transform(current_history, &event) + }) + .await + } + + async fn update_transaction_by_hash_index( + &self, + tx_id: &TransactionId, + bundle_id: BundleId, + ) -> Result<()> { + let s3_key = S3Key::TransactionByHash(tx_id.hash); + let key = s3_key.to_string(); + + self.idempotent_write::(&key, |current_metadata| { + update_transaction_metadata_transform(current_metadata, bundle_id) + }) + .await + } + + async fn idempotent_write(&self, key: &str, mut transform_fn: F) -> Result<()> + where + T: for<'de> Deserialize<'de> + Serialize + Clone + Default + Debug, + F: FnMut(T) -> Option, + { + const MAX_RETRIES: usize = 5; + const BASE_DELAY_MS: u64 = 100; + + for attempt in 0..MAX_RETRIES { + let (current_value, etag) = self.get_object_with_etag::(key).await?; + let value = current_value.unwrap_or_default(); + + match transform_fn(value.clone()) { + Some(new_value) => { + let content = serde_json::to_string(&new_value)?; + + let mut put_request = self + .s3_client + .put_object() + .bucket(&self.bucket) + .key(key) + .body(ByteStream::from(content.into_bytes())); + + if let Some(etag) = etag { + put_request = put_request.if_match(etag); + } else { + put_request = put_request.if_none_match("*"); + } + + match put_request.send().await { + Ok(_) => { + info!( + s3_key = %key, + attempt = attempt + 1, + "Successfully wrote object with idempotent write" + ); + return Ok(()); + } + Err(e) => { + if attempt < MAX_RETRIES - 1 { + let delay = BASE_DELAY_MS * 2_u64.pow(attempt as u32); + info!( + s3_key = %key, + attempt = attempt + 1, + delay_ms = delay, + error = %e, + "Conflict detected, retrying with backoff" + ); + tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await; + } else { + return Err(anyhow::anyhow!( + "Failed to write after {} attempts: {}", + MAX_RETRIES, + e + )); + } + } + } + } + None => { + info!( + s3_key = %key, + "Transform function returned None, no write required" + ); + return Ok(()); + } + } + } + + Err(anyhow::anyhow!("Exceeded maximum retry attempts")) + } + + async fn get_object_with_etag(&self, key: &str) -> Result<(Option, Option)> + where + T: for<'de> Deserialize<'de>, + { + match self + .s3_client + .get_object() + .bucket(&self.bucket) + .key(key) + .send() + .await + { + Ok(response) => { + let etag = response.e_tag().map(|s| s.to_string()); + let body = response.body.collect().await?; + let content = String::from_utf8(body.into_bytes().to_vec())?; + let value: T = serde_json::from_str(&content)?; + Ok((Some(value), etag)) + } + Err(e) => match &e { + SdkError::ServiceError(service_err) => match service_err.err() { + GetObjectError::NoSuchKey(_) => Ok((None, None)), + _ => Err(anyhow::anyhow!("Failed to get object: {}", e)), + }, + _ => { + let error_string = e.to_string(); + if error_string.contains("NoSuchKey") + || error_string.contains("NotFound") + || error_string.contains("404") + { + Ok((None, None)) + } else { + Err(anyhow::anyhow!("Failed to get object: {}", e)) + } + } + }, + } + } +} + +#[async_trait] +impl MempoolEventWriter for S3MempoolEventReaderWriter { + async fn archive_event(&self, event: Event) -> Result<()> { + let bundle_id = event.event.bundle_id(); + let transaction_ids = event.event.transaction_ids(); + + self.update_bundle_history(event.clone()).await?; + + for tx_id in &transaction_ids { + self.update_transaction_by_hash_index(tx_id, bundle_id) + .await?; + } + + Ok(()) + } +} + +#[async_trait] +impl MempoolEventS3Reader for S3MempoolEventReaderWriter { + async fn get_bundle_history(&self, bundle_id: BundleId) -> Result> { + let s3_key = S3Key::Bundle(bundle_id).to_string(); + let (bundle_history, _) = self.get_object_with_etag::(&s3_key).await?; + Ok(bundle_history) + } + + async fn get_transaction_metadata( + &self, + tx_hash: TxHash, + ) -> Result> { + let s3_key = S3Key::TransactionByHash(tx_hash).to_string(); + let (transaction_metadata, _) = self + .get_object_with_etag::(&s3_key) + .await?; + Ok(transaction_metadata) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::reader::Event; + use crate::types::{DropReason, MempoolEvent}; + use alloy_primitives::TxHash; + use alloy_rpc_types_mev::EthSendBundle; + use uuid::Uuid; + + fn create_test_bundle() -> EthSendBundle { + EthSendBundle::default() + } + + fn create_test_event(key: &str, timestamp: i64, mempool_event: MempoolEvent) -> Event { + Event { + key: key.to_string(), + timestamp, + event: mempool_event, + } + } + + #[test] + fn test_update_bundle_history_transform_adds_new_event() { + let bundle_history = BundleHistory { history: vec![] }; + let bundle = create_test_bundle(); + let bundle_id = Uuid::new_v4(); + let mempool_event = MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }; + let event = create_test_event("test-key", 1234567890, mempool_event); + + let result = update_bundle_history_transform(bundle_history, &event); + + assert!(result.is_some()); + let bundle_history = result.unwrap(); + assert_eq!(bundle_history.history.len(), 1); + + match &bundle_history.history[0] { + BundleHistoryEvent::Created { + key, + timestamp: ts, + bundle: b, + } => { + assert_eq!(key, "test-key"); + assert_eq!(*ts, 1234567890); + assert_eq!(b.block_number, bundle.block_number); + } + _ => panic!("Expected Created event"), + } + } + + #[test] + fn test_update_bundle_history_transform_skips_duplicate_key() { + let existing_event = BundleHistoryEvent::Created { + key: "duplicate-key".to_string(), + timestamp: 1111111111, + bundle: create_test_bundle(), + }; + let bundle_history = BundleHistory { + history: vec![existing_event], + }; + + let bundle = create_test_bundle(); + let bundle_id = Uuid::new_v4(); + let mempool_event = MempoolEvent::Updated { bundle_id, bundle }; + let event = create_test_event("duplicate-key", 1234567890, mempool_event); + + let result = update_bundle_history_transform(bundle_history, &event); + + assert!(result.is_none()); + } + + #[test] + fn test_update_bundle_history_transform_handles_all_event_types() { + let bundle_history = BundleHistory { history: vec![] }; + let bundle_id = Uuid::new_v4(); + + // Test Created + let bundle = create_test_bundle(); + let mempool_event = MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }; + let event = create_test_event("test-key", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test Updated + let mempool_event = MempoolEvent::Updated { + bundle_id, + bundle: bundle.clone(), + }; + let event = create_test_event("test-key-2", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test Cancelled + let mempool_event = MempoolEvent::Cancelled { bundle_id }; + let event = create_test_event("test-key-3", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test BuilderIncluded + let mempool_event = MempoolEvent::BuilderIncluded { + bundle_id, + builder: "test-builder".to_string(), + block_number: 12345, + flashblock_index: 1, + }; + let event = create_test_event("test-key-4", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test FlashblockIncluded + let mempool_event = MempoolEvent::FlashblockIncluded { + bundle_id, + block_number: 12345, + flashblock_index: 1, + }; + let event = create_test_event("test-key-5", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test BlockIncluded + let mempool_event = MempoolEvent::BlockIncluded { + bundle_id, + block_number: 12345, + block_hash: TxHash::from([1u8; 32]), + }; + let event = create_test_event("test-key-6", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + // Test Dropped + let mempool_event = MempoolEvent::Dropped { + bundle_id, + reason: DropReason::TimedOut, + }; + let event = create_test_event("test-key-7", 1234567890, mempool_event); + let result = update_bundle_history_transform(bundle_history, &event); + assert!(result.is_some()); + } + + #[test] + fn test_update_transaction_metadata_transform_adds_new_bundle() { + let metadata = TransactionMetadata { bundle_ids: vec![] }; + let bundle_id = Uuid::new_v4(); + + let result = update_transaction_metadata_transform(metadata, bundle_id); + + assert!(result.is_some()); + let metadata = result.unwrap(); + assert_eq!(metadata.bundle_ids.len(), 1); + assert_eq!(metadata.bundle_ids[0], bundle_id); + } + + #[test] + fn test_update_transaction_metadata_transform_skips_existing_bundle() { + let bundle_id = Uuid::new_v4(); + let metadata = TransactionMetadata { + bundle_ids: vec![bundle_id], + }; + + let result = update_transaction_metadata_transform(metadata, bundle_id); + + assert!(result.is_none()); + } + + #[test] + fn test_update_transaction_metadata_transform_adds_to_existing_bundles() { + let existing_bundle_id = Uuid::new_v4(); + let new_bundle_id = Uuid::new_v4(); + let metadata = TransactionMetadata { + bundle_ids: vec![existing_bundle_id], + }; + + let result = update_transaction_metadata_transform(metadata, new_bundle_id); + + assert!(result.is_some()); + let metadata = result.unwrap(); + assert_eq!(metadata.bundle_ids.len(), 2); + assert!(metadata.bundle_ids.contains(&existing_bundle_id)); + assert!(metadata.bundle_ids.contains(&new_bundle_id)); + } +} diff --git a/crates/audit/src/types.rs b/crates/audit/src/types.rs new file mode 100644 index 0000000..add8114 --- /dev/null +++ b/crates/audit/src/types.rs @@ -0,0 +1,116 @@ +use alloy_consensus::transaction::{SignerRecoverable, Transaction as ConsensusTransaction}; +use alloy_primitives::{Address, TxHash, U256}; +use alloy_provider::network::eip2718::Decodable2718; +use alloy_rpc_types_mev::EthSendBundle; +use bytes::Bytes; +use op_alloy_consensus::OpTxEnvelope; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TransactionId { + pub sender: Address, + pub nonce: U256, + pub hash: TxHash, +} + +pub type BundleId = Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DropReason { + TimedOut, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bundle { + pub id: BundleId, + pub transactions: Vec, + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + pub id: TransactionId, + pub data: Bytes, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum MempoolEvent { + Created { + bundle_id: BundleId, + bundle: EthSendBundle, + }, + Updated { + bundle_id: BundleId, + bundle: EthSendBundle, + }, + Cancelled { + bundle_id: BundleId, + }, + BuilderIncluded { + bundle_id: BundleId, + builder: String, + block_number: u64, + flashblock_index: u64, + }, + FlashblockIncluded { + bundle_id: BundleId, + block_number: u64, + flashblock_index: u64, + }, + BlockIncluded { + bundle_id: BundleId, + block_number: u64, + block_hash: TxHash, + }, + Dropped { + bundle_id: BundleId, + reason: DropReason, + }, +} + +impl MempoolEvent { + pub fn bundle_id(&self) -> BundleId { + match self { + MempoolEvent::Created { bundle_id, .. } => *bundle_id, + MempoolEvent::Updated { bundle_id, .. } => *bundle_id, + MempoolEvent::Cancelled { bundle_id, .. } => *bundle_id, + MempoolEvent::BuilderIncluded { bundle_id, .. } => *bundle_id, + MempoolEvent::FlashblockIncluded { bundle_id, .. } => *bundle_id, + MempoolEvent::BlockIncluded { bundle_id, .. } => *bundle_id, + MempoolEvent::Dropped { bundle_id, .. } => *bundle_id, + } + } + + pub fn transaction_ids(&self) -> Vec { + match self { + MempoolEvent::Created { bundle, .. } | MempoolEvent::Updated { bundle, .. } => { + bundle + .txs + .iter() + .filter_map(|tx_bytes| { + match OpTxEnvelope::decode_2718_exact(tx_bytes.iter().as_slice()) { + Ok(envelope) => { + match envelope.recover_signer() { + Ok(sender) => Some(TransactionId { + sender, + nonce: U256::from(envelope.nonce()), + hash: *envelope.hash(), + }), + Err(_) => None, // Skip invalid transactions + } + } + Err(_) => None, // Skip malformed transactions + } + }) + .collect() + } + MempoolEvent::Cancelled { .. } => vec![], + MempoolEvent::BuilderIncluded { .. } => vec![], + MempoolEvent::FlashblockIncluded { .. } => vec![], + MempoolEvent::BlockIncluded { .. } => vec![], + MempoolEvent::Dropped { .. } => vec![], + } + } +} diff --git a/crates/audit/tests/common/mod.rs b/crates/audit/tests/common/mod.rs new file mode 100644 index 0000000..46fa30b --- /dev/null +++ b/crates/audit/tests/common/mod.rs @@ -0,0 +1,78 @@ +use rdkafka::producer::FutureProducer; +use rdkafka::{consumer::StreamConsumer, ClientConfig}; +use testcontainers::runners::AsyncRunner; +use testcontainers_modules::{kafka, kafka::Kafka, minio::MinIO}; +use uuid::Uuid; + +pub struct TestHarness { + pub s3_client: aws_sdk_s3::Client, + pub bucket_name: String, + #[allow(dead_code)] // TODO is read + pub kafka_producer: FutureProducer, + #[allow(dead_code)] // TODO is read + pub kafka_consumer: StreamConsumer, + _minio_container: testcontainers::ContainerAsync, + _kafka_container: testcontainers::ContainerAsync, +} + +impl TestHarness { + pub async fn new() -> Result> { + let minio_container = MinIO::default().start().await?; + let s3_port = minio_container.get_host_port_ipv4(9000).await?; + let s3_endpoint = format!("http://127.0.0.1:{}", s3_port); + + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region("us-east-1") + .endpoint_url(&s3_endpoint) + .credentials_provider(aws_sdk_s3::config::Credentials::new( + "minioadmin", + "minioadmin", + None, + None, + "test", + )) + .load() + .await; + + let s3_client = aws_sdk_s3::Client::new(&config); + let bucket_name = format!("test-bucket-{}", Uuid::new_v4()); + + s3_client + .create_bucket() + .bucket(&bucket_name) + .send() + .await?; + + let kafka_container = Kafka::default().start().await?; + let bootstrap_servers = format!( + "127.0.0.1:{}", + kafka_container + .get_host_port_ipv4(kafka::KAFKA_PORT) + .await? + ); + + let kafka_producer = ClientConfig::new() + .set("bootstrap.servers", &bootstrap_servers) + .set("message.timeout.ms", "5000") + .create::() + .expect("Failed to create Kafka FutureProducer"); + + let kafka_consumer = ClientConfig::new() + .set("group.id", "testcontainer-rs") + .set("bootstrap.servers", &bootstrap_servers) + .set("session.timeout.ms", "6000") + .set("enable.auto.commit", "false") + .set("auto.offset.reset", "earliest") + .create::() + .expect("Failed to create Kafka StreamConsumer"); + + Ok(TestHarness { + s3_client, + bucket_name, + kafka_producer, + kafka_consumer, + _minio_container: minio_container, + _kafka_container: kafka_container, + }) + } +} diff --git a/crates/audit/tests/integration_tests.rs b/crates/audit/tests/integration_tests.rs new file mode 100644 index 0000000..ba41479 --- /dev/null +++ b/crates/audit/tests/integration_tests.rs @@ -0,0 +1,73 @@ +use alloy_rpc_types_mev::EthSendBundle; +use std::time::Duration; +use tips_audit::{ + publisher::{KafkaMempoolEventPublisher, MempoolEventPublisher}, + storage::{MempoolEventS3Reader, S3MempoolEventReaderWriter}, + types::{DropReason, MempoolEvent}, + KafkaMempoolArchiver, KafkaMempoolReader, +}; +use uuid::Uuid; +mod common; +use common::TestHarness; + +#[tokio::test] +async fn test_kafka_publisher_s3_archiver_integration( +) -> Result<(), Box> { + let harness = TestHarness::new().await?; + let topic = "test-mempool-events"; + + let s3_writer = + S3MempoolEventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let test_bundle_id = Uuid::new_v4(); + let test_events = vec![ + MempoolEvent::Created { + bundle_id: test_bundle_id, + bundle: EthSendBundle::default(), + }, + MempoolEvent::Dropped { + bundle_id: test_bundle_id, + reason: DropReason::TimedOut, + }, + ]; + + let publisher = KafkaMempoolEventPublisher::new(harness.kafka_producer, topic.to_string()); + + for event in test_events.iter() { + publisher.publish(event.clone()).await?; + } + + let mut consumer = KafkaMempoolArchiver::new( + KafkaMempoolReader::new(harness.kafka_consumer, topic.to_string())?, + s3_writer.clone(), + ); + + tokio::spawn(async move { + consumer.run().await.expect("error running consumer"); + }); + + // Wait for the messages to be received + let mut counter = 0; + loop { + counter += 1; + if counter > 10 { + assert!(false, "unable to complete archiving within the deadline"); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + let bundle_history = s3_writer.get_bundle_history(test_bundle_id).await?; + + if bundle_history.is_some() { + let history = bundle_history.unwrap(); + if history.history.len() != test_events.len() { + continue; + } else { + break; + } + } else { + continue; + } + } + + Ok(()) +} diff --git a/crates/audit/tests/s3_test.rs b/crates/audit/tests/s3_test.rs new file mode 100644 index 0000000..171da55 --- /dev/null +++ b/crates/audit/tests/s3_test.rs @@ -0,0 +1,273 @@ +use alloy_primitives::{b256, bytes, Bytes, TxHash}; +use alloy_rpc_types_mev::EthSendBundle; +use std::sync::Arc; +use tips_audit::{ + reader::Event, + storage::{MempoolEventS3Reader, MempoolEventWriter, S3MempoolEventReaderWriter}, + types::MempoolEvent, +}; +use tokio::task::JoinSet; +use uuid::Uuid; + +mod common; +use common::TestHarness; + +// https://basescan.org/tx/0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e +const TXN_DATA: Bytes = bytes!("0x02f88f8221058304b6b3018315fb3883124f80948ff2f0a8d017c79454aa28509a19ab9753c2dd1480a476d58e1a0182426068c9ea5b00000000000000000002f84f00000000083e4fda54950000c080a086fbc7bbee41f441fb0f32f7aa274d2188c460fe6ac95095fa6331fa08ec4ce7a01aee3bcc3c28f7ba4e0c24da9ae85e9e0166c73cabb42c25ff7b5ecd424f3105"); +const TXN_HASH: TxHash = + b256!("0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e"); + +fn create_test_bundle() -> EthSendBundle { + EthSendBundle { + txs: vec![TXN_DATA.clone()], + ..Default::default() + } +} + +fn create_test_event(key: &str, timestamp: i64, mempool_event: MempoolEvent) -> Event { + Event { + key: key.to_string(), + timestamp, + event: mempool_event, + } +} + +#[tokio::test] +async fn test_event_write_and_read() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = + S3MempoolEventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle_id = Uuid::new_v4(); + let bundle = create_test_bundle(); + let event = create_test_event( + "test-key-1", + 1234567890, + MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }, + ); + + writer.archive_event(event).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "test-key-1"); + + let metadata = writer.get_transaction_metadata(TXN_HASH).await?; + assert!(metadata.is_some()); + + if let Some(metadata) = metadata { + assert!(metadata.bundle_ids.contains(&bundle_id)); + } + + let bundle_id_two = Uuid::new_v4(); + let bundle = create_test_bundle(); + let event = create_test_event( + "test-key-2", + 1234567890, + MempoolEvent::Created { + bundle_id: bundle_id_two, + bundle: bundle.clone(), + }, + ); + + writer.archive_event(event).await?; + + let metadata = writer.get_transaction_metadata(TXN_HASH).await?; + assert!(metadata.is_some()); + + if let Some(metadata) = metadata { + assert!(metadata.bundle_ids.contains(&bundle_id)); + assert!(metadata.bundle_ids.contains(&bundle_id_two)); + } + + Ok(()) +} + +#[tokio::test] +async fn test_events_appended() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = + S3MempoolEventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle_id = Uuid::new_v4(); + let bundle = create_test_bundle(); + + let events = vec![ + create_test_event( + "test-key-1", + 1234567890, + MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }, + ), + create_test_event( + "test-key-2", + 1234567891, + MempoolEvent::Updated { + bundle_id, + bundle: bundle.clone(), + }, + ), + create_test_event( + "test-key-3", + 1234567892, + MempoolEvent::Cancelled { bundle_id }, + ), + ]; + + for (idx, event) in events.iter().enumerate() { + writer.archive_event(event.clone()).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), idx + 1); + + let keys: Vec = history + .history + .iter() + .map(|e| e.key().to_string()) + .collect(); + assert_eq!( + keys, + events + .iter() + .map(|e| e.key.clone()) + .take(idx + 1) + .collect::>() + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_event_deduplication() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = + S3MempoolEventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle_id = Uuid::new_v4(); + let bundle = create_test_bundle(); + let event = create_test_event( + "duplicate-key", + 1234567890, + MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }, + ); + + writer.archive_event(event.clone()).await?; + writer.archive_event(event).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "duplicate-key"); + + Ok(()) +} + +#[tokio::test] +async fn test_nonexistent_data() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = + S3MempoolEventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let nonexistent_bundle_id = Uuid::new_v4(); + let bundle_history = writer.get_bundle_history(nonexistent_bundle_id).await?; + assert!(bundle_history.is_none()); + + let nonexistent_tx_hash = TxHash::from([255u8; 32]); + let metadata = writer.get_transaction_metadata(nonexistent_tx_hash).await?; + assert!(metadata.is_none()); + + Ok(()) +} + +#[tokio::test] +#[ignore = "TODO doesn't appear to work with minio, should test against a real S3 bucket"] +async fn test_concurrent_writes_for_bundle() -> Result<(), Box> +{ + let harness = TestHarness::new().await?; + let writer = Arc::new(S3MempoolEventReaderWriter::new( + harness.s3_client.clone(), + harness.bucket_name.clone(), + )); + + let bundle_id = Uuid::new_v4(); + let bundle = create_test_bundle(); + + let event = create_test_event( + "hello-dan", + 1234567889i64, + MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }, + ); + + writer.archive_event(event.clone()).await?; + + let mut join_set = JoinSet::new(); + + for i in 0..4 { + let writer_clone = writer.clone(); + let key = if i % 4 == 0 { + "shared-key".to_string() + } else { + format!("unique-key-{}", i) + }; + + let event = create_test_event( + &key, + 1234567890 + i as i64, + MempoolEvent::Created { + bundle_id, + bundle: bundle.clone(), + }, + ); + + join_set.spawn(async move { writer_clone.archive_event(event.clone()).await }); + } + + let tasks = join_set.join_all().await; + assert_eq!(tasks.len(), 4); + for t in tasks.iter() { + assert!(t.is_ok()); + } + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + + let shared_count = history + .history + .iter() + .filter(|e| e.key() == "shared-key") + .count(); + assert_eq!(shared_count, 1); + + let unique_count = history + .history + .iter() + .filter(|e| e.key().starts_with("unique-key-")) + .count(); + assert_eq!(unique_count, 3); + + assert_eq!(history.history.len(), 4); + + Ok(()) +} diff --git a/crates/datastore/Cargo.toml b/crates/datastore/Cargo.toml new file mode 100644 index 0000000..a77c22c --- /dev/null +++ b/crates/datastore/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tips-datastore" +version = "0.1.0" +edition = "2024" + +[dependencies] +sqlx.workspace = true +uuid.workspace = true +tokio.workspace = true +anyhow.workspace = true +async-trait.workspace = true +alloy-rpc-types-mev.workspace = true +alloy-primitives.workspace = true +alloy-consensus.workspace = true +op-alloy-consensus.workspace = true +eyre.workspace = true +tracing.workspace = true + +[dev-dependencies] +testcontainers.workspace = true +testcontainers-modules.workspace = true diff --git a/crates/datastore/migrations/1757444171_create_bundles_table.sql b/crates/datastore/migrations/1757444171_create_bundles_table.sql new file mode 100644 index 0000000..a5fcdf7 --- /dev/null +++ b/crates/datastore/migrations/1757444171_create_bundles_table.sql @@ -0,0 +1,18 @@ +-- Create bundles table +CREATE TABLE IF NOT EXISTS bundles ( + id UUID PRIMARY KEY, + + senders CHAR(42)[], + minimum_base_fee BIGINT, -- todo find a larger type + txn_hashes CHAR(66)[], + + txs TEXT[] NOT NULL, + reverting_tx_hashes CHAR(66)[], + dropping_tx_hashes CHAR(66)[], + + block_number BIGINT, + min_timestamp BIGINT, + max_timestamp BIGINT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); \ No newline at end of file diff --git a/crates/datastore/migrations/init-db.sh b/crates/datastore/migrations/init-db.sh new file mode 100755 index 0000000..ac13ad2 --- /dev/null +++ b/crates/datastore/migrations/init-db.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +echo "Running database migrations..." + +for file in /migrations/*.sql; do + if [ -f "$file" ]; then + echo "Applying migration: $(basename "$file")" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < "$file" + fi +done + +echo "Database initialization completed successfully" \ No newline at end of file diff --git a/crates/datastore/src/lib.rs b/crates/datastore/src/lib.rs new file mode 100644 index 0000000..25abc97 --- /dev/null +++ b/crates/datastore/src/lib.rs @@ -0,0 +1,4 @@ +pub mod postgres; +pub mod traits; +pub use postgres::PostgresDatastore; +pub use traits::BundleDatastore; diff --git a/crates/datastore/src/postgres.rs b/crates/datastore/src/postgres.rs new file mode 100644 index 0000000..88b75ad --- /dev/null +++ b/crates/datastore/src/postgres.rs @@ -0,0 +1,319 @@ +use crate::traits::BundleDatastore; +use alloy_consensus::Transaction; +use alloy_consensus::private::alloy_eips::Decodable2718; +use alloy_consensus::transaction::SignerRecoverable; +use alloy_primitives::hex::{FromHex, ToHexExt}; +use alloy_primitives::{Address, TxHash}; +use alloy_rpc_types_mev::EthSendBundle; +use anyhow::Result; +use op_alloy_consensus::OpTxEnvelope; +use sqlx::PgPool; +use tracing::info; +use uuid::Uuid; + +#[derive(sqlx::FromRow, Debug)] +struct BundleRow { + senders: Option>, + minimum_base_fee: Option, + txn_hashes: Option>, + txs: Vec, + reverting_tx_hashes: Option>, + dropping_tx_hashes: Option>, + block_number: Option, + min_timestamp: Option, + max_timestamp: Option, +} + +/// Filter criteria for selecting bundles +#[derive(Debug, Clone, Default)] +pub struct BundleFilter { + pub base_fee: Option, + pub block_number: Option, + pub timestamp: Option, +} + +impl BundleFilter { + pub fn new() -> Self { + Self::default() + } + + pub fn with_base_fee(mut self, base_fee: i64) -> Self { + self.base_fee = Some(base_fee); + self + } + + pub fn valid_for_block(mut self, block_number: u64) -> Self { + self.block_number = Some(block_number); + self + } + + pub fn valid_for_timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = Some(timestamp); + self + } +} + +/// Extended bundle data that includes the original bundle plus extracted metadata +#[derive(Debug, Clone)] +pub struct BundleWithMetadata { + pub bundle: EthSendBundle, + pub txn_hashes: Vec, + pub senders: Vec
, + pub min_base_fee: i64, +} + +/// PostgreSQL implementation of the BundleDatastore trait +#[derive(Debug, Clone)] +pub struct PostgresDatastore { + pool: PgPool, +} + +impl PostgresDatastore { + pub async fn run_migrations(&self) -> Result<()> { + info!(message = "running migrations"); + sqlx::migrate!("./migrations").run(&self.pool).await?; + info!(message = "migrations complete"); + Ok(()) + } + + pub async fn connect(url: String) -> Result { + let pool = PgPool::connect(&url).await?; + Ok(Self::new(pool)) + } + + /// Create a new PostgreSQL datastore instance + pub fn new(pool: PgPool) -> Self { + Self { pool } + } + + fn row_to_bundle_with_metadata(&self, row: BundleRow) -> Result { + let parsed_txs: Result, _> = + row.txs.into_iter().map(|tx_hex| tx_hex.parse()).collect(); + + let parsed_reverting_tx_hashes: Result, _> = row + .reverting_tx_hashes + .unwrap_or_default() + .into_iter() + .map(TxHash::from_hex) + .collect(); + + let parsed_dropping_tx_hashes: Result, _> = row + .dropping_tx_hashes + .unwrap_or_default() + .into_iter() + .map(TxHash::from_hex) + .collect(); + + let bundle = EthSendBundle { + txs: parsed_txs?, + block_number: row.block_number.unwrap_or(0) as u64, + min_timestamp: row.min_timestamp.map(|t| t as u64), + max_timestamp: row.max_timestamp.map(|t| t as u64), + reverting_tx_hashes: parsed_reverting_tx_hashes?, + replacement_uuid: None, + dropping_tx_hashes: parsed_dropping_tx_hashes?, + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: Vec::new(), + extra_fields: Default::default(), + }; + + let parsed_txn_hashes: Result, _> = row + .txn_hashes + .unwrap_or_default() + .into_iter() + .map(TxHash::from_hex) + .collect(); + + let parsed_senders: Result, _> = row + .senders + .unwrap_or_default() + .into_iter() + .map(Address::from_hex) + .collect(); + + Ok(BundleWithMetadata { + bundle, + txn_hashes: parsed_txn_hashes?, + senders: parsed_senders?, + min_base_fee: row.minimum_base_fee.unwrap_or(0), + }) + } + + fn extract_bundle_metadata( + &self, + bundle: &EthSendBundle, + ) -> Result<(Vec, i64, Vec)> { + let mut senders = Vec::new(); + let mut txn_hashes = Vec::new(); + + let mut min_base_fee = i64::MAX; + + for tx_bytes in &bundle.txs { + let envelope = OpTxEnvelope::decode_2718_exact(tx_bytes)?; + txn_hashes.push(envelope.hash().encode_hex_with_prefix()); + + let sender = match envelope.recover_signer() { + Ok(signer) => signer, + Err(err) => return Err(err.into()), + }; + + senders.push(sender.encode_hex_with_prefix()); + min_base_fee = min_base_fee.min(envelope.max_fee_per_gas() as i64); // todo type and todo not right + } + + let minimum_base_fee = if min_base_fee == i64::MAX { + 0 + } else { + min_base_fee + }; + + Ok((senders, minimum_base_fee, txn_hashes)) + } +} + +#[async_trait::async_trait] +impl BundleDatastore for PostgresDatastore { + async fn insert_bundle(&self, bundle: EthSendBundle) -> Result { + let id = Uuid::new_v4(); + + let (senders, minimum_base_fee, txn_hashes) = self.extract_bundle_metadata(&bundle)?; + + let txs: Vec = bundle + .txs + .iter() + .map(|tx| tx.encode_hex_upper_with_prefix()) + .collect(); + let reverting_tx_hashes: Vec = bundle + .reverting_tx_hashes + .iter() + .map(|h| h.encode_hex_with_prefix()) + .collect(); + let dropping_tx_hashes: Vec = bundle + .dropping_tx_hashes + .iter() + .map(|h| h.encode_hex_with_prefix()) + .collect(); + + sqlx::query!( + r#" + INSERT INTO bundles ( + id, senders, minimum_base_fee, txn_hashes, + txs, reverting_tx_hashes, dropping_tx_hashes, + block_number, min_timestamp, max_timestamp, + created_at, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) + "#, + id, + &senders, + minimum_base_fee, + &txn_hashes, + &txs, + &reverting_tx_hashes, + &dropping_tx_hashes, + bundle.block_number as i64, + bundle.min_timestamp.map(|t| t as i64), + bundle.max_timestamp.map(|t| t as i64), + ) + .execute(&self.pool) + .await?; + + Ok(id) + } + + async fn get_bundle(&self, id: Uuid) -> Result> { + let result = sqlx::query_as::<_, BundleRow>( + r#" + SELECT senders, minimum_base_fee, txn_hashes, txs, reverting_tx_hashes, + dropping_tx_hashes, block_number, min_timestamp, max_timestamp + FROM bundles + WHERE id = $1 + "#, + ) + .bind(id) + .fetch_optional(&self.pool) + .await?; + + match result { + Some(row) => { + let bundle_with_metadata = self.row_to_bundle_with_metadata(row)?; + Ok(Some(bundle_with_metadata)) + } + None => Ok(None), + } + } + + async fn cancel_bundle(&self, id: Uuid) -> Result<()> { + sqlx::query("DELETE FROM bundles WHERE id = $1") + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn select_bundles(&self, filter: BundleFilter) -> Result> { + let base_fee = filter.base_fee.unwrap_or(0); + let block_number = filter.block_number.unwrap_or(0) as i64; + + let (min_ts, max_ts) = if let Some(timestamp) = filter.timestamp { + (timestamp as i64, timestamp as i64) + } else { + // If not specified, set the parameters to be the whole range + (i64::MAX, 0i64) + }; + + let rows = sqlx::query_as::<_, BundleRow>( + r#" + SELECT senders, minimum_base_fee, txn_hashes, txs, reverting_tx_hashes, + dropping_tx_hashes, block_number, min_timestamp, max_timestamp + FROM bundles + WHERE minimum_base_fee >= $1 + AND (block_number = $2 OR block_number IS NULL OR block_number = 0 OR $2 = 0) + AND (min_timestamp <= $3 OR min_timestamp IS NULL) + AND (max_timestamp >= $4 OR max_timestamp IS NULL) + ORDER BY minimum_base_fee DESC + "#, + ) + .bind(base_fee) + .bind(block_number) + .bind(min_ts) + .bind(max_ts) + .fetch_all(&self.pool) + .await?; + + let mut bundles = Vec::new(); + for row in rows { + let bundle_with_metadata = self.row_to_bundle_with_metadata(row)?; + bundles.push(bundle_with_metadata); + } + + Ok(bundles) + } + + async fn find_bundle_by_transaction_hash(&self, tx_hash: TxHash) -> Result> { + let tx_hash_str = tx_hash.encode_hex_with_prefix(); + + let result = sqlx::query_scalar::<_, Uuid>( + r#" + SELECT id + FROM bundles + WHERE $1 = ANY(txn_hashes) + LIMIT 1 + "#, + ) + .bind(&tx_hash_str) + .fetch_optional(&self.pool) + .await?; + + Ok(result) + } + + async fn remove_bundle(&self, id: Uuid) -> Result<()> { + sqlx::query("DELETE FROM bundles WHERE id = $1") + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } +} diff --git a/crates/datastore/src/traits.rs b/crates/datastore/src/traits.rs new file mode 100644 index 0000000..e5e58f2 --- /dev/null +++ b/crates/datastore/src/traits.rs @@ -0,0 +1,27 @@ +use crate::postgres::{BundleFilter, BundleWithMetadata}; +use alloy_primitives::TxHash; +use alloy_rpc_types_mev::EthSendBundle; +use anyhow::Result; +use uuid::Uuid; + +/// Trait defining the interface for bundle datastore operations +#[async_trait::async_trait] +pub trait BundleDatastore: Send + Sync { + /// Insert a new bundle into the datastore + async fn insert_bundle(&self, bundle: EthSendBundle) -> Result; + + /// Fetch a bundle with metadata by its ID + async fn get_bundle(&self, id: Uuid) -> Result>; + + /// Cancel a bundle by UUID + async fn cancel_bundle(&self, id: Uuid) -> Result<()>; + + /// Select the candidate bundles to include in the next Flashblock + async fn select_bundles(&self, filter: BundleFilter) -> Result>; + + /// Find bundle ID by transaction hash + async fn find_bundle_by_transaction_hash(&self, tx_hash: TxHash) -> Result>; + + /// Remove a bundle by ID + async fn remove_bundle(&self, id: Uuid) -> Result<()>; +} diff --git a/crates/datastore/tests/datastore.rs b/crates/datastore/tests/datastore.rs new file mode 100644 index 0000000..534292f --- /dev/null +++ b/crates/datastore/tests/datastore.rs @@ -0,0 +1,304 @@ +use alloy_primitives::{Address, Bytes, TxHash}; +use alloy_rpc_types_mev::EthSendBundle; +use sqlx::PgPool; +use testcontainers_modules::{ + postgres, + testcontainers::{ContainerAsync, runners::AsyncRunner}, +}; +use tips_datastore::postgres::BundleFilter; +use tips_datastore::{BundleDatastore, PostgresDatastore}; + +struct TestHarness { + _postgres_instance: ContainerAsync, + data_store: PostgresDatastore, +} + +async fn setup_datastore() -> eyre::Result { + let postgres_instance = postgres::Postgres::default().start().await?; + let connection_string = format!( + "postgres://postgres:postgres@{}:{}/postgres", + postgres_instance.get_host().await?, + postgres_instance.get_host_port_ipv4(5432).await? + ); + + let pool = PgPool::connect(&connection_string).await?; + let data_store = PostgresDatastore::new(pool); + + assert!(data_store.run_migrations().await.is_ok()); + Ok(TestHarness { + _postgres_instance: postgres_instance, + data_store, + }) +} + +fn get_test_tx() -> eyre::Result { + "0x02f8bf8221058304f8c782038c83d2a76b833d0900942e85c218afcdeb3d3b3f0f72941b4861f915bbcf80b85102000e0000000bb800001010c78c430a094eb7ae67d41a7cca25cdb9315e63baceb03bf4529e57a6b1b900010001f4000a101010110111101111011011faa7efc8e6aa13b029547eecbf5d370b4e1e52eec080a009fc02a6612877cec7e1223f0a14f9a9507b82ef03af41fcf14bf5018ccf2242a0338b46da29a62d28745c828077327588dc82c03a4b0210e3ee1fd62c608f8fcd".parse::().map_err(|e| e.into()) +} + +fn create_test_bundle_with_reverting_tx() -> eyre::Result { + Ok(EthSendBundle { + txs: vec![get_test_tx()?], + block_number: 12345, + min_timestamp: Some(1640995200), + max_timestamp: Some(1640995260), + reverting_tx_hashes: vec![ + "0x3ea7e1482485387e61150ee8e5c8cad48a14591789ac02cc2504046d96d0a5f4" + .parse::()?, + ], + replacement_uuid: None, + dropping_tx_hashes: vec![], + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: vec![], + extra_fields: Default::default(), + }) +} + +fn create_test_bundle( + block_number: u64, + min_timestamp: Option, + max_timestamp: Option, +) -> eyre::Result { + Ok(EthSendBundle { + txs: vec![get_test_tx()?], + block_number, + min_timestamp, + max_timestamp, + reverting_tx_hashes: vec![], + replacement_uuid: None, + dropping_tx_hashes: vec![], + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: vec![], + extra_fields: Default::default(), + }) +} + +#[tokio::test] +async fn insert_and_get() -> eyre::Result<()> { + let harness = setup_datastore().await?; + let test_bundle = create_test_bundle_with_reverting_tx()?; + + let insert_result = harness.data_store.insert_bundle(test_bundle.clone()).await; + if let Err(ref err) = insert_result { + eprintln!("Insert failed with error: {:?}", err); + } + assert!(insert_result.is_ok()); + let bundle_id = insert_result.unwrap(); + + let query_result = harness.data_store.get_bundle(bundle_id).await; + assert!(query_result.is_ok()); + let retrieved_bundle_with_metadata = query_result.unwrap(); + + assert!( + retrieved_bundle_with_metadata.is_some(), + "Bundle should be found" + ); + let metadata = retrieved_bundle_with_metadata.unwrap(); + let retrieved_bundle = &metadata.bundle; + + assert_eq!(retrieved_bundle.txs.len(), test_bundle.txs.len()); + assert_eq!(retrieved_bundle.block_number, test_bundle.block_number); + assert_eq!(retrieved_bundle.min_timestamp, test_bundle.min_timestamp); + assert_eq!(retrieved_bundle.max_timestamp, test_bundle.max_timestamp); + assert_eq!( + retrieved_bundle.reverting_tx_hashes.len(), + test_bundle.reverting_tx_hashes.len() + ); + assert_eq!( + retrieved_bundle.dropping_tx_hashes.len(), + test_bundle.dropping_tx_hashes.len() + ); + + assert!( + !metadata.txn_hashes.is_empty(), + "Transaction hashes should not be empty" + ); + assert!(!metadata.senders.is_empty(), "Senders should not be empty"); + assert_eq!( + metadata.txn_hashes.len(), + 1, + "Should have one transaction hash" + ); + assert_eq!(metadata.senders.len(), 1, "Should have one sender"); + assert!( + metadata.min_base_fee >= 0, + "Min base fee should be non-negative" + ); + + let expected_hash: TxHash = + "0x3ea7e1482485387e61150ee8e5c8cad48a14591789ac02cc2504046d96d0a5f4".parse()?; + let expected_sender: Address = "0x24ae36512421f1d9f6e074f00ff5b8393f5dd925".parse()?; + + assert_eq!( + metadata.txn_hashes[0], expected_hash, + "Transaction hash should match" + ); + assert_eq!(metadata.senders[0], expected_sender, "Sender should match"); + + Ok(()) +} + +#[tokio::test] +async fn select_bundles_comprehensive() -> eyre::Result<()> { + let harness = setup_datastore().await?; + + let bundle1 = create_test_bundle(100, Some(1000), Some(2000))?; + let bundle2 = create_test_bundle(200, Some(1500), Some(2500))?; + let bundle3 = create_test_bundle(300, None, None)?; // valid for all times + let bundle4 = create_test_bundle(0, Some(500), Some(3000))?; // valid for all blocks + + harness + .data_store + .insert_bundle(bundle1) + .await + .expect("Failed to insert bundle1"); + harness + .data_store + .insert_bundle(bundle2) + .await + .expect("Failed to insert bundle2"); + harness + .data_store + .insert_bundle(bundle3) + .await + .expect("Failed to insert bundle3"); + harness + .data_store + .insert_bundle(bundle4) + .await + .expect("Failed to insert bundle4"); + + let empty_filter = BundleFilter::new(); + let all_bundles = harness + .data_store + .select_bundles(empty_filter) + .await + .expect("Failed to select bundles with empty filter"); + assert_eq!( + all_bundles.len(), + 4, + "Should return all 4 bundles with empty filter" + ); + + let block_filter = BundleFilter::new().valid_for_block(200); + let filtered_bundles = harness + .data_store + .select_bundles(block_filter) + .await + .expect("Failed to select bundles with block filter"); + assert_eq!( + filtered_bundles.len(), + 2, + "Should return 2 bundles for block 200 (bundle2 + bundle4 with block 0)" + ); + assert_eq!(filtered_bundles[0].bundle.block_number, 200); + + let timestamp_filter = BundleFilter::new().valid_for_timestamp(1500); + let timestamp_filtered = harness + .data_store + .select_bundles(timestamp_filter) + .await + .expect("Failed to select bundles with timestamp filter"); + assert_eq!( + timestamp_filtered.len(), + 4, + "Should return all 4 bundles (all contain timestamp 1500: bundle1[1000-2000], bundle2[1500-2500], bundle3[NULL-NULL], bundle4[500-3000])" + ); + + let combined_filter = BundleFilter::new() + .valid_for_block(200) + .valid_for_timestamp(2000); + let combined_filtered = harness + .data_store + .select_bundles(combined_filter) + .await + .expect("Failed to select bundles with combined filter"); + assert_eq!( + combined_filtered.len(), + 2, + "Should return 2 bundles (bundle2: block=200 and timestamp range 1500-2500 contains 2000; bundle4: block=0 matches all blocks and timestamp range 500-3000 contains 2000)" + ); + assert_eq!(combined_filtered[0].bundle.block_number, 200); + + let no_match_filter = BundleFilter::new().valid_for_block(999); + let no_matches = harness + .data_store + .select_bundles(no_match_filter) + .await + .expect("Failed to select bundles with no match filter"); + assert_eq!( + no_matches.len(), + 1, + "Should return 1 bundle for non-existent block (bundle4 with block 0 is valid for all blocks)" + ); + + Ok(()) +} + +#[tokio::test] +async fn cancel_bundle_workflow() -> eyre::Result<()> { + let harness = setup_datastore().await?; + + let bundle1 = create_test_bundle(100, Some(1000), Some(2000))?; + let bundle2 = create_test_bundle(200, Some(1500), Some(2500))?; + + let bundle1_id = harness + .data_store + .insert_bundle(bundle1) + .await + .expect("Failed to insert bundle1"); + let bundle2_id = harness + .data_store + .insert_bundle(bundle2) + .await + .expect("Failed to insert bundle2"); + + let retrieved_bundle1 = harness + .data_store + .get_bundle(bundle1_id) + .await + .expect("Failed to get bundle1"); + assert!( + retrieved_bundle1.is_some(), + "Bundle1 should exist before cancellation" + ); + + let retrieved_bundle2 = harness + .data_store + .get_bundle(bundle2_id) + .await + .expect("Failed to get bundle2"); + assert!( + retrieved_bundle2.is_some(), + "Bundle2 should exist before cancellation" + ); + + harness + .data_store + .cancel_bundle(bundle1_id) + .await + .expect("Failed to cancel bundle1"); + + let cancelled_bundle1 = harness + .data_store + .get_bundle(bundle1_id) + .await + .expect("Failed to get bundle1 after cancellation"); + assert!( + cancelled_bundle1.is_none(), + "Bundle1 should not exist after cancellation" + ); + + let still_exists_bundle2 = harness + .data_store + .get_bundle(bundle2_id) + .await + .expect("Failed to get bundle2 after bundle1 cancellation"); + assert!( + still_exists_bundle2.is_some(), + "Bundle2 should still exist after bundle1 cancellation" + ); + + Ok(()) +} diff --git a/crates/ingress/Cargo.toml b/crates/ingress/Cargo.toml new file mode 100644 index 0000000..3ed9fc3 --- /dev/null +++ b/crates/ingress/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "tips-ingress" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "tips-ingress" +path = "src/main.rs" + +[dependencies] +tips-datastore.workspace = true +tips-audit.workspace = true +jsonrpsee.workspace = true +alloy-rpc-types-mev.workspace = true +alloy-primitives.workspace = true +op-alloy-network.workspace = true +alloy-provider.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +anyhow.workspace = true +clap.workspace = true +url.workspace = true +alloy-consensus.workspace = true +op-alloy-consensus.workspace = true +eyre.workspace = true +dotenvy.workspace = true +rdkafka.workspace = true \ No newline at end of file diff --git a/crates/ingress/src/main.rs b/crates/ingress/src/main.rs new file mode 100644 index 0000000..c38ab8b --- /dev/null +++ b/crates/ingress/src/main.rs @@ -0,0 +1,120 @@ +use alloy_provider::{ProviderBuilder, RootProvider}; +use clap::Parser; +use jsonrpsee::server::Server; +use op_alloy_network::Optimism; +use rdkafka::ClientConfig; +use rdkafka::producer::FutureProducer; +use std::net::IpAddr; +use tips_audit::KafkaMempoolEventPublisher; +use tracing::{info, warn}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use url::Url; + +mod service; +use service::{IngressApiServer, IngressService}; +use tips_datastore::PostgresDatastore; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Config { + /// Address to bind the RPC server to + #[arg(long, env = "TIPS_INGRESS_ADDRESS", default_value = "127.0.0.1")] + address: IpAddr, + + /// Port to bind the RPC server to + #[arg(long, env = "TIPS_INGRESS_PORT", default_value = "8080")] + port: u16, + + /// URL of the mempool service to proxy transactions to + #[arg(long, env = "TIPS_INGRESS_RPC_MEMPOOL")] + mempool_url: Url, + + /// URL of the Postgres DB to store bundles in + #[arg(long, env = "TIPS_INGRESS_DATABASE_URL")] + database_url: String, + + /// Enable dual writing raw transactions to the mempool + #[arg(long, env = "TIPS_INGRESS_DUAL_WRITE_MEMPOOL", default_value = "false")] + dual_write_mempool: bool, + + /// Kafka brokers for publishing mempool events + #[arg(long, env = "TIPS_INGRESS_KAFKA_BROKERS")] + kafka_brokers: String, + + /// Kafka topic for publishing mempool events + #[arg( + long, + env = "TIPS_INGRESS_KAFKA_TOPIC", + default_value = "mempool-events" + )] + kafka_topic: String, + + #[arg(long, env = "TIPS_INGRESS_LOG_LEVEL", default_value = "info")] + log_level: String, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenvy::dotenv().ok(); + + let config = Config::parse(); + + let log_level = match config.log_level.to_lowercase().as_str() { + "trace" => tracing::Level::TRACE, + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => { + warn!( + "Invalid log level '{}', defaulting to 'info'", + config.log_level + ); + tracing::Level::INFO + } + }; + + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level.to_string())), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + info!( + message = "Starting ingress service", + address = %config.address, + port = config.port, + mempool_url = %config.mempool_url + ); + + let provider: RootProvider = ProviderBuilder::new() + .disable_recommended_fillers() + .network::() + .connect_http(config.mempool_url); + + let bundle_store = PostgresDatastore::connect(config.database_url).await?; + bundle_store.run_migrations().await?; + + let kafka_producer: FutureProducer = ClientConfig::new() + .set("bootstrap.servers", &config.kafka_brokers) + .set("message.timeout.ms", "5000") + .create()?; + + let publisher = KafkaMempoolEventPublisher::new(kafka_producer, config.kafka_topic); + + let service = IngressService::new(provider, bundle_store, config.dual_write_mempool, publisher); + let bind_addr = format!("{}:{}", config.address, config.port); + + let server = Server::builder().build(&bind_addr).await?; + let addr = server.local_addr()?; + let handle = server.start(service.into_rpc()); + + info!( + message = "Ingress RPC server started", + address = %addr + ); + + handle.stopped().await; + Ok(()) +} diff --git a/crates/ingress/src/service.rs b/crates/ingress/src/service.rs new file mode 100644 index 0000000..18e4911 --- /dev/null +++ b/crates/ingress/src/service.rs @@ -0,0 +1,133 @@ +use alloy_primitives::{B256, Bytes}; +use alloy_provider::network::eip2718::Decodable2718; +use alloy_provider::{Provider, RootProvider}; +use alloy_rpc_types_mev::{EthBundleHash, EthCancelBundle, EthSendBundle}; +use jsonrpsee::types::ErrorObject; +use jsonrpsee::{ + core::{RpcResult, async_trait}, + proc_macros::rpc, +}; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_network::Optimism; +use tips_audit::{MempoolEvent, MempoolEventPublisher}; +use tips_datastore::BundleDatastore; +use tracing::{info, warn}; + +#[rpc(server, namespace = "eth")] +pub trait IngressApi { + /// `eth_sendBundle` can be used to send your bundles to the builder. + #[method(name = "sendBundle")] + async fn send_bundle(&self, bundle: EthSendBundle) -> RpcResult; + + /// `eth_cancelBundle` is used to prevent a submitted bundle from being included on-chain. + #[method(name = "cancelBundle")] + async fn cancel_bundle(&self, request: EthCancelBundle) -> RpcResult<()>; + + /// Handler for: `eth_sendRawTransaction` + #[method(name = "sendRawTransaction")] + async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult; +} + +pub struct IngressService { + provider: RootProvider, + datastore: Store, + dual_write_mempool: bool, + publisher: Publisher, +} + +impl IngressService { + pub fn new( + provider: RootProvider, + datastore: Store, + dual_write_mempool: bool, + publisher: Publisher, + ) -> Self { + Self { + provider, + datastore, + dual_write_mempool, + publisher, + } + } +} + +#[async_trait] +impl IngressApiServer for IngressService +where + Store: BundleDatastore + Sync + Send + 'static, + Publisher: MempoolEventPublisher + Sync + Send + 'static, +{ + async fn send_bundle(&self, _bundle: EthSendBundle) -> RpcResult { + warn!( + message = "TODO: implement send_bundle", + method = "send_bundle" + ); + todo!("implement send_bundle") + } + + async fn cancel_bundle(&self, _request: EthCancelBundle) -> RpcResult<()> { + warn!( + message = "TODO: implement cancel_bundle", + method = "cancel_bundle" + ); + todo!("implement cancel_bundle") + } + + async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult { + let envelope = OpTxEnvelope::decode_2718_exact(tx.iter().as_slice()) + .map_err(|_e| ErrorObject::owned(10, "todo", Some(1)))?; + + // TODO: Validation and simulation + + // TODO: parallelize DB and mempool setup + let bundle = EthSendBundle { + txs: vec![tx.clone()], + block_number: 0, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: vec![envelope.tx_hash()], + ..Default::default() + }; + + let result = self + .datastore + .insert_bundle(bundle.clone()) + .await + .map_err(|_e| ErrorObject::owned(11, "todo", Some(2)))?; + + info!(message="inserted singleton bundle", uuid=%result, txn_hash=%envelope.tx_hash()); + + if let Err(e) = self + .publisher + .publish(MempoolEvent::Created { + bundle_id: result, + bundle, + }) + .await + { + warn!(message = "Failed to publish MempoolEvent::Created", error = %e); + } + + if self.dual_write_mempool { + // If we also want to dual write to the mempool + let response = self + .provider + .send_raw_transaction(tx.iter().as_slice()) + .await; + + match response { + Ok(_) => { + info!(message = "sent transaction to the mempool", hash=%envelope.tx_hash()); + } + Err(e) => { + warn!( + message = "Failed to send raw transaction to mempool", + error = %e + ); + } + } + } + + Ok(envelope.tx_hash()) + } +} diff --git a/crates/maintenance/Cargo.toml b/crates/maintenance/Cargo.toml new file mode 100644 index 0000000..9d7eec6 --- /dev/null +++ b/crates/maintenance/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "tips-maintenance" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "tips-maintenance" +path = "src/main.rs" + +[dependencies] +tips-datastore.workspace = true +tips-audit.workspace = true +alloy-provider.workspace = true +alloy-primitives.workspace = true +alloy-rpc-types.workspace = true +op-alloy-network.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +anyhow.workspace = true +clap.workspace = true +dotenvy.workspace = true +rdkafka.workspace = true +serde_json.workspace = true +url.workspace = true \ No newline at end of file diff --git a/crates/maintenance/src/main.rs b/crates/maintenance/src/main.rs new file mode 100644 index 0000000..9fcbc01 --- /dev/null +++ b/crates/maintenance/src/main.rs @@ -0,0 +1,188 @@ +use alloy_provider::network::TransactionResponse; +use alloy_provider::network::primitives::BlockTransactions; +use alloy_provider::{Provider, ProviderBuilder, RootProvider}; +use anyhow::Result; +use clap::Parser; +use op_alloy_network::Optimism; +use rdkafka::ClientConfig; +use rdkafka::producer::FutureProducer; +use std::time::Duration; +use tips_audit::{KafkaMempoolEventPublisher, MempoolEvent, MempoolEventPublisher}; +use tips_datastore::{BundleDatastore, PostgresDatastore}; +use tokio::time::sleep; +use tracing::{error, info, warn}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use url::Url; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(long, env = "TIPS_MAINTENANCE_RPC_NODE")] + node_url: Url, + + #[arg(long, env = "TIPS_MAINTENANCE_KAFKA_BROKERS")] + kafka_brokers: String, + + #[arg( + long, + env = "TIPS_MAINTENANCE_KAFKA_TOPIC", + default_value = "mempool-events" + )] + kafka_topic: String, + + #[arg(long, env = "TIPS_MAINTENANCE_DATABASE_URL")] + database_url: String, + + #[arg(long, env = "TIPS_MAINTENANCE_POLL_INTERVAL_MS", default_value = "250")] + poll_interval: u64, + + #[arg(long, env = "TIPS_MAINTENANCE_LOG_LEVEL", default_value = "info")] + log_level: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + + let args = Args::parse(); + + let log_level = match args.log_level.to_lowercase().as_str() { + "trace" => tracing::Level::TRACE, + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => { + warn!( + "Invalid log level '{}', defaulting to 'info'", + args.log_level + ); + tracing::Level::INFO + } + }; + + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level.to_string())), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + info!("Starting maintenance service"); + + let provider: RootProvider = ProviderBuilder::new() + .disable_recommended_fillers() + .network::() + .connect_http(args.node_url); + + let datastore = PostgresDatastore::connect(args.database_url).await?; + + let kafka_producer: FutureProducer = ClientConfig::new() + .set("bootstrap.servers", &args.kafka_brokers) + .set("message.timeout.ms", "5000") + .create()?; + + let publisher = KafkaMempoolEventPublisher::new(kafka_producer, args.kafka_topic); + + let mut last_processed_block: Option = None; + + loop { + match process_new_blocks(&provider, &datastore, &publisher, &mut last_processed_block).await + { + Ok(_) => {} + Err(e) => { + error!(message = "Error processing blocks", error=%e); + } + } + + sleep(Duration::from_millis(args.poll_interval)).await; + } +} + +async fn process_new_blocks( + provider: &impl Provider, + datastore: &PostgresDatastore, + publisher: &KafkaMempoolEventPublisher, + last_processed_block: &mut Option, +) -> Result<()> { + let latest_block_number = provider.get_block_number().await?; + + let start_block = last_processed_block + .map(|n| n + 1) + .unwrap_or(latest_block_number); + + if start_block > latest_block_number { + return Ok(()); + } + + info!(message = "Processing blocks", from=%start_block, to=%latest_block_number); + + for block_number in start_block..=latest_block_number { + match process_block(provider, datastore, publisher, block_number).await { + Ok(_) => { + info!(message = "Successfully processed block", block=%block_number); + *last_processed_block = Some(block_number); + } + Err(e) => { + return Err(e); + } + } + } + + Ok(()) +} + +async fn process_block( + provider: &impl Provider, + datastore: &PostgresDatastore, + publisher: &KafkaMempoolEventPublisher, + block_number: u64, +) -> Result<()> { + let block = provider + .get_block_by_number(block_number.into()) + .full() + .await? + .ok_or_else(|| anyhow::anyhow!("Block {} not found", block_number))?; + + let block_hash = block.header.hash; + + let transactions = match &block.transactions { + BlockTransactions::Full(txs) => txs, + BlockTransactions::Hashes(_) => { + return Err(anyhow::anyhow!( + "Block transactions returned as hashes only, expected full transactions" + )); + } + BlockTransactions::Uncle => { + return Err(anyhow::anyhow!("Block contains uncle transactions")); + } + }; + + for tx in transactions { + let tx_hash = tx.tx_hash(); + info!(message = "Processing transaction", tx_hash=%tx_hash); + + match datastore.find_bundle_by_transaction_hash(tx_hash).await? { + Some(bundle_id) => { + info!(message = "Found bundle for transaction", bundle_id=%bundle_id, tx_hash=%tx_hash); + + let event = MempoolEvent::BlockIncluded { + bundle_id, + block_number, + block_hash, + }; + + publisher.publish(event).await?; + datastore.remove_bundle(bundle_id).await?; + + info!(message = "Found bundle for transaction", bundle_id=%bundle_id, tx_hash=%tx_hash); + } + None => { + error!(message = "Transaction not part of tracked bundle", tx_hash=%tx_hash); + } + } + } + + Ok(()) +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2b94f8d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,94 @@ +services: + postgres: + image: postgres:16 + container_name: tips-db + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + POSTGRES_USER: postgres + ports: + - "5432:5432" + volumes: + - ./data/postgres:/var/lib/postgresql/data + - ./crates/datastore/migrations/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh + - ./crates/datastore/migrations:/migrations + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + kafka: + image: confluentinc/cp-kafka:7.5.0 + container_name: tips-kafka + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093,PLAINTEXT_HOST://0.0.0.0:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_ZOOKEEPER_CONNECT: ' ' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_NODE_ID: 1 + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_LOG_DIRS: /var/lib/kafka/data + CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw + volumes: + - ./data/kafka:/var/lib/kafka/data + healthcheck: + test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"] + interval: 10s + timeout: 10s + retries: 5 + + kafka-setup: + image: confluentinc/cp-kafka:7.5.0 + container_name: tips-kafka-setup + depends_on: + kafka: + condition: service_healthy + command: | + sh -c " + kafka-topics --create --if-not-exists --topic tips-audit --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 + kafka-topics --list --bootstrap-server kafka:29092 + " + + minio: + image: minio/minio:latest + container_name: tips-minio + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "7000:9000" + - "7001:9001" + command: server /data --console-address ":9001" + volumes: + - ./data/minio:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + + minio-setup: + image: minio/mc + container_name: tips-minio-setup + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set minio http://minio:9000 minioadmin minioadmin; + /usr/bin/mc mb minio/tips; + /usr/bin/mc anonymous set public minio/tips; + exit 0; + " + diff --git a/justfile b/justfile new file mode 100644 index 0000000..41456c0 --- /dev/null +++ b/justfile @@ -0,0 +1,104 @@ +# Run all CI checks locally +ci: rust-ci ui-ci + +# Run Rust CI checks +rust-ci: check test fmt clippy build + +# Run UI CI checks +ui-ci: ui-format ui-lint-check ui-typecheck ui-build + +# Run UI formatting +ui-format: + cd ui && npm run format + +# Run UI linting with fixes +ui-lint: + cd ui && npx biome check --write + +# Run UI linting (check only) +ui-lint-check: + cd ui && npm run lint + +# Run UI TypeScript type checking +ui-typecheck: + cd ui && npx tsc --noEmit + +# Build UI for production +ui-build: + cd ui && npm run build + +create-migration name: + touch crates/datastore/migrations/$(date +%s)_{{ name }}.sql + +# Pull database schema using drizzle-kit +sync-db-schema: + # Rust + cargo sqlx prepare -D postgresql://postgres:postgres@localhost:5432/postgres --workspace --all --no-dotenv + # NextJS + cd ui && npx drizzle-kit pull --dialect=postgresql --url=postgresql://postgres:postgres@localhost:5432/postgres + cd ui && mv ./drizzle/relations.ts ./src/db/ + cd ui && mv ./drizzle/schema.ts ./src/db/ + cd ui && rm -rf ./drizzle + +sync-env: + cp .env.example .env + cp .env.example ./ui/.env + +# Check code compilation +check: + cargo check + +# Run tests +test: + cargo test + +# Check formatting +fmt: + cargo fmt --all -- --check + +# Run clippy lints +clippy: + cargo clippy -- -D warnings + +# Build release binary +build: + cargo build + +deps-reset: + docker compose down && docker compose rm && rm -rf data/ && mkdir -p data/postgres data/kafka data/minio && docker compose up -d + +deps: + docker compose down && docker compose rm && docker compose up -d + +# Run the ingress service with default mempool URL +ingress: + cargo run --bin tips-ingress + +# Run the archiver service +audit: + cargo run --bin tips-audit + +# Run the maintenance service +maintenance: + cargo run --bin tips-maintenance + +ui: + cd ui && yarn dev + +# Run autofixes everything +fix: fix-rust fix-ui + +# Fix Rust code issues +fix-rust: fmt-fix clippy-fix + +# Fix UI code issues +fix-ui: + cd ui && npx biome check --fix + +# Format code (fix) +fmt-fix: + cargo fmt --all + +# Run clippy with fixes +clippy-fix: + cargo clippy --fix --allow-dirty --allow-staged \ No newline at end of file diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..0c62030 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,13 @@ +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` diff --git a/ui/biome.json b/ui/biome.json new file mode 100644 index 0000000..41b3b95 --- /dev/null +++ b/ui/biome.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noUnknownAtRules": "off" + } + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/ui/drizzle.config.ts b/ui/drizzle.config.ts new file mode 100644 index 0000000..2ae4233 --- /dev/null +++ b/ui/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./drizzle", + dialect: "postgresql", + dbCredentials: { + url: + process.env.TIPS_DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/postgres", + }, +}); diff --git a/ui/next.config.ts b/ui/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/ui/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..e337743 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,4803 @@ +{ + "name": "ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui", + "version": "0.1.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.888.0", + "drizzle-kit": "^0.31.4", + "drizzle-orm": "^0.44.5", + "next": "15.5.3", + "pg": "^8.16.3", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/pg": "^8.15.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.888.0.tgz", + "integrity": "sha512-MgYyF/qpvCMYVSiOpRJ5C/EtdFxuYAeF5SprtMsbf71xBiiCH5GurB616i+ZxJqHlfhBQTTvR0qugnWvk1Wqvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-node": "3.888.0", + "@aws-sdk/middleware-bucket-endpoint": "3.887.0", + "@aws-sdk/middleware-expect-continue": "3.887.0", + "@aws-sdk/middleware-flexible-checksums": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-location-constraint": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-sdk-s3": "3.888.0", + "@aws-sdk/middleware-ssec": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/signature-v4-multi-region": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@aws-sdk/xml-builder": "3.887.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/eventstream-serde-browser": "^4.1.1", + "@smithy/eventstream-serde-config-resolver": "^4.2.1", + "@smithy/eventstream-serde-node": "^4.1.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-blob-browser": "^4.1.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/hash-stream-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/md5-js": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "@smithy/util-waiter": "^4.1.1", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.888.0.tgz", + "integrity": "sha512-8CLy/ehGKUmekjH+VtZJ4w40PqDg3u0K7uPziq/4P8Q7LLgsy8YQoHNbuY4am7JU3HWrqLXJI9aaz1+vPGPoWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.888.0.tgz", + "integrity": "sha512-L3S2FZywACo4lmWv37Y4TbefuPJ1fXWyWwIJ3J4wkPYFJ47mmtUPqThlVrSbdTHkEjnZgJe5cRfxk0qCLsFh1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws-sdk/xml-builder": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.888.0.tgz", + "integrity": "sha512-shPi4AhUKbIk7LugJWvNpeZA8va7e5bOHAEKo89S0Ac8WDZt2OaNzbh/b9l0iSL2eEyte8UgIsYGcFxOwIF1VA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.888.0.tgz", + "integrity": "sha512-Jvuk6nul0lE7o5qlQutcqlySBHLXOyoPtiwE6zyKbGc7RVl0//h39Lab7zMeY2drMn8xAnIopL4606Fd8JI/Hw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.888.0.tgz", + "integrity": "sha512-M82ItvS5yq+tO6ZOV1ruaVs2xOne+v8HW85GFCXnz8pecrzYdgxh6IsVqEbbWruryG/mUGkWMbkBZoEsy4MgyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.888.0.tgz", + "integrity": "sha512-KCrQh1dCDC8Y+Ap3SZa6S81kHk+p+yAaOQ5jC3dak4zhHW3RCrsGR/jYdemTOgbEGcA6ye51UbhWfrrlMmeJSA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.888.0", + "@aws-sdk/credential-provider-http": "3.888.0", + "@aws-sdk/credential-provider-ini": "3.888.0", + "@aws-sdk/credential-provider-process": "3.888.0", + "@aws-sdk/credential-provider-sso": "3.888.0", + "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.888.0.tgz", + "integrity": "sha512-+aX6piSukPQ8DUS4JAH344GePg8/+Q1t0+kvSHAZHhYvtQ/1Zek3ySOJWH2TuzTPCafY4nmWLcQcqvU1w9+4Lw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.888.0.tgz", + "integrity": "sha512-b1ZJji7LJ6E/j1PhFTyvp51in2iCOQ3VP6mj5H6f5OUnqn7efm41iNMoinKr87n0IKZw7qput5ggXVxEdPhouA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.888.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/token-providers": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.888.0.tgz", + "integrity": "sha512-7P0QNtsDzMZdmBAaY/vY1BsZHwTGvEz3bsn2bm5VSKFAeMmZqsHK1QeYdNsFjLtegnVh+wodxMq50jqLv3LFlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.887.0.tgz", + "integrity": "sha512-qRCte/3MtNiMhPh4ZEGk9cHfAXq6IDTflvi2t1tkOIVZFyshkSCvNQNJrrE2D/ljVbOK1f3XbBDaF43EoQzIRQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.887.0.tgz", + "integrity": "sha512-AlrTZZScDTG9SYeT82BC5cK/6Q4N0miN5xqMW/pbBqP3fNXlsdJOWKB+EKD3V6DV41EV5GVKHKe/1065xKSQ4w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.888.0.tgz", + "integrity": "sha512-vdwd4wMAlXSg1bldhXyTsDSnyPP+bbEVihapejGKNd4gLfyyHwjTfbli+B/hEONGttQs5Dp54UMn8yW/UA189g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.887.0.tgz", + "integrity": "sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.887.0.tgz", + "integrity": "sha512-eU/9Cq4gg2sS32bOomxdx2YF43kb+o70pMhnEBBnVVeqzE8co78SO5FQdWfRTfhNJgTyQ6Vgosx//CNMPIfZPg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.887.0.tgz", + "integrity": "sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.887.0.tgz", + "integrity": "sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.888.0.tgz", + "integrity": "sha512-rKOFNfqgqOfrdcLGF8fcO75azWS2aq2ksRHFoIEFru5FJxzu/yDAhY4C2FKiP/X34xeIUS2SbE/gQgrgWHSN2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.11.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.887.0.tgz", + "integrity": "sha512-1ixZks0IDkdac1hjPe4vdLSuD9HznkhblCEb4T0wNyw3Ee1fdXg+MlcPWywzG5zkPXLcIrULUzJg/OSYfaDXcQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.888.0.tgz", + "integrity": "sha512-ZkcUkoys8AdrNNG7ATjqw2WiXqrhTvT+r4CIK3KhOqIGPHX0p0DQWzqjaIl7ZhSUToKoZ4Ud7MjF795yUr73oA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@smithy/core": "^3.11.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.888.0.tgz", + "integrity": "sha512-py4o4RPSGt+uwGvSBzR6S6cCBjS4oTX5F8hrHFHfPCdIOMVjyOBejn820jXkCrcdpSj3Qg1yUZXxsByvxc9Lyg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.888.0", + "@aws-sdk/middleware-host-header": "3.887.0", + "@aws-sdk/middleware-logger": "3.887.0", + "@aws-sdk/middleware-recursion-detection": "3.887.0", + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/types": "3.887.0", + "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-user-agent-browser": "3.887.0", + "@aws-sdk/util-user-agent-node": "3.888.0", + "@smithy/config-resolver": "^4.2.1", + "@smithy/core": "^3.11.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.1", + "@smithy/util-defaults-mode-node": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.887.0.tgz", + "integrity": "sha512-VdSMrIqJ3yjJb/fY+YAxrH/lCVv0iL8uA+lbMNfQGtO5tB3Zx6SU9LEpUwBNX8fPK1tUpI65CNE4w42+MY/7Mg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.888.0.tgz", + "integrity": "sha512-FmOHUaJzEhqfcpyh0L7HLwYcYopK13Dbmuf+oUyu56/RoeB1nLnltH1VMQVj8v3Am2IwlGR+/JpFyrdkErN+cA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.888.0.tgz", + "integrity": "sha512-WA3NF+3W8GEuCMG1WvkDYbB4z10G3O8xuhT7QSjhvLYWQ9CPt3w4VpVIfdqmUn131TCIbhCzD0KN/1VJTjAjyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.888.0", + "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.887.0.tgz", + "integrity": "sha512-fmTEJpUhsPsovQ12vZSpVTEP/IaRoJAMBGQXlQNjtCpkBp6Iq3KQDa/HDaPINE+3xxo6XvTdtibsNOd5zJLV9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.887.0.tgz", + "integrity": "sha512-kpegvT53KT33BMeIcGLPA65CQVxLUL/C3gTz9AzlU/SDmeusBHX4nRApAicNzI/ltQ5lxZXbQn18UczzBuwF1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.887.0.tgz", + "integrity": "sha512-X71UmVsYc6ZTH4KU6hA5urOzYowSXc3qvroagJNLJYU1ilgZ529lP4J9XOYfEvTXkLR1hPFSRxa43SrwgelMjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.887.0", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.888.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.888.0.tgz", + "integrity": "sha512-rSB3OHyuKXotIGfYEo//9sU0lXAUrTY28SUUnxzOGYuQsAt0XR5iYwBAp+RjV6x8f+Hmtbg0PdCsy1iNAXa0UQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/types": "3.887.0", + "@smithy/node-config-provider": "^4.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.887.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.887.0.tgz", + "integrity": "sha512-lMwgWK1kNgUhHGfBvO/5uLe7TKhycwOn3eRCqsKPT9aPCx/HWuTlpcQp8oW2pCRGLS7qzcxqpQulcD+bbUL7XQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.0.tgz", + "integrity": "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.2.0", + "@biomejs/cli-darwin-x64": "2.2.0", + "@biomejs/cli-linux-arm64": "2.2.0", + "@biomejs/cli-linux-arm64-musl": "2.2.0", + "@biomejs/cli-linux-x64": "2.2.0", + "@biomejs/cli-linux-x64-musl": "2.2.0", + "@biomejs/cli-win32-arm64": "2.2.0", + "@biomejs/cli-win32-x64": "2.2.0" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.0.tgz", + "integrity": "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.0.tgz", + "integrity": "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.0.tgz", + "integrity": "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.0.tgz", + "integrity": "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.0.tgz", + "integrity": "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.0.tgz", + "integrity": "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.0.tgz", + "integrity": "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "license": "Apache-2.0" + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", + "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", + "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", + "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", + "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", + "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", + "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", + "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", + "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", + "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.1.0.tgz", + "integrity": "sha512-a36AtR7Q7XOhRPt6F/7HENmTWcB8kN7mDJcOFM/+FuKO6x88w8MQJfYCufMWh4fGyVkPjUh3Rrz/dnqFQdo6OQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.1.0.tgz", + "integrity": "sha512-Bnv0B3nSlfB2mPO0WgM49I/prl7+kamF042rrf3ezJ3Z4C7csPYvyYgZfXTGXwXfj1mAwDWjE/ybIf49PzFzvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", + "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.1", + "@smithy/util-utf8": "^4.1.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.1.1.tgz", + "integrity": "sha512-PwkQw1hZwHTQB6X5hSUWz2OSeuj5Z6enWuAqke7DgWoP3t6vg3ktPpqPz3Erkn6w+tmsl8Oss6nrgyezoea2Iw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.1.1.tgz", + "integrity": "sha512-Q9QWdAzRaIuVkefupRPRFAasaG/droBqn1feiMnmLa+LLEUG45pqX1+FurHFmlqiCfobB3nUlgoJfeXZsr7MPA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.2.1.tgz", + "integrity": "sha512-oSUkF9zDN9zcOUBMtxp8RewJlh71E9NoHWU8jE3hU9JMYCsmW4assVTpgic/iS3/dM317j6hO5x18cc3XrfvEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.1.1.tgz", + "integrity": "sha512-tn6vulwf/ScY0vjhzptSJuDJJqlhNtUjkxJ4wiv9E3SPoEqTEKbaq6bfqRO7nvhTG29ALICRcvfFheOUPl8KNA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.1.1.tgz", + "integrity": "sha512-uLOAiM/Dmgh2CbEXQx+6/ssK7fbzFhd+LjdyFxXid5ZBCbLHTFHLdD/QbXw5aEDsLxQhgzDxLLsZhsftAYwHJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.1.1.tgz", + "integrity": "sha512-avAtk++s1e/1VODf+rg7c9R2pB5G9y8yaJaGY4lPZI2+UIqVyuSDMikWjeWfBVmFZ3O7NpDxBbUCyGhThVUKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.1.0", + "@smithy/chunked-blob-reader-native": "^4.1.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.1.1.tgz", + "integrity": "sha512-3ztT4pV0Moazs3JAYFdfKk11kYFDo4b/3R3+xVjIm6wY9YpJf+xfz+ocEnNKcWAdcmSMqi168i2EMaKmJHbJMA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.1.1.tgz", + "integrity": "sha512-MvWXKK743BuHjr/hnWuT6uStdKEaoqxHAQUvbKJPPZM5ZojTNFI5D+47BoQfBE5RgGlRRty05EbWA+NXDv+hIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.2.tgz", + "integrity": "sha512-M51KcwD+UeSOFtpALGf5OijWt915aQT5eJhqnMKJt7ZTfDfNcvg2UZgIgTZUoiORawb6o5lk4n3rv7vnzQXgsA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.2.tgz", + "integrity": "sha512-KZJueEOO+PWqflv2oGx9jICpHdBYXwCI19j7e2V3IMwKgFcXc9D9q/dsTf4B+uCnYxjNoS1jpyv6pGNGRsKOXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.1", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", + "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.2.tgz", + "integrity": "sha512-u82cjh/x7MlMat76Z38TRmEcG6JtrrxN4N2CSNG5o2v2S3hfLAxRgSgFqf0FKM3dglH41Evknt/HOX+7nfzZ3g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.11.0", + "@smithy/middleware-endpoint": "^4.2.2", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.2.tgz", + "integrity": "sha512-QKrOw01DvNHKgY+3p4r9Ut4u6EHLVZ01u6SkOMe6V6v5C+nRPXJeWh72qCT1HgwU3O7sxAIu23nNh+FOpYVZKA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.2.tgz", + "integrity": "sha512-l2yRmSfx5haYHswPxMmCR6jGwgPs5LjHLuBwlj9U7nNBMS43YV/eevj+Xq1869UYdiynnMrCKtoOYQcwtb6lKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", + "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", + "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.1.1.tgz", + "integrity": "sha512-PJBmyayrlfxM7nbqjomF4YcT1sApQwZio0NHSsT0EzhJqljRmvhzqZua43TyEs80nJk2Cn2FGPg/N8phH6KeCQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", + "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "postcss": "^8.4.41", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@types/node": { + "version": "20.19.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.14.tgz", + "integrity": "sha512-gqiKWld3YIkmtrrg9zDvg9jfksZCcPywXVN7IauUGhilwGV/yOyeUsvpR796m/Jye0zUzMXPKe8Ct1B79A7N5Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", + "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/drizzle-kit": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz", + "integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==", + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.5", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", + "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", + "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.3", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.3", + "@next/swc-darwin-x64": "15.5.3", + "@next/swc-linux-arm64-gnu": "15.5.3", + "@next/swc-linux-arm64-musl": "15.5.3", + "@next/swc-linux-x64-gnu": "15.5.3", + "@next/swc-linux-x64-musl": "15.5.3", + "@next/swc-win32-arm64-msvc": "15.5.3", + "@next/swc-win32-x64-msvc": "15.5.3", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..a44dcb4 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,31 @@ +{ + "name": "ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build --turbopack", + "start": "next start", + "lint": "biome check", + "format": "biome format --write" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.888.0", + "drizzle-kit": "^0.31.4", + "drizzle-orm": "^0.44.5", + "next": "15.5.3", + "pg": "^8.16.3", + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/pg": "^8.15.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/ui/postcss.config.mjs b/ui/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/ui/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/ui/public/file.svg b/ui/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/ui/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/globe.svg b/ui/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/ui/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/next.svg b/ui/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/ui/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/vercel.svg b/ui/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/ui/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/window.svg b/ui/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/ui/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/app/api/bundle/[uuid]/route.ts b/ui/src/app/api/bundle/[uuid]/route.ts new file mode 100644 index 0000000..999dec3 --- /dev/null +++ b/ui/src/app/api/bundle/[uuid]/route.ts @@ -0,0 +1,34 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { type BundleEvent, getBundleHistory } from "@/lib/s3"; + +export interface BundleHistoryResponse { + uuid: string; + history: BundleEvent[]; +} + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ uuid: string }> }, +) { + try { + const { uuid } = await params; + + const bundle = await getBundleHistory(uuid); + if (!bundle) { + return NextResponse.json({ error: "Bundle not found" }, { status: 404 }); + } + + const response: BundleHistoryResponse = { + uuid, + history: bundle.history, + }; + + return NextResponse.json(response); + } catch (error) { + console.error("Error fetching bundle data:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/ui/src/app/api/bundles/all/route.ts b/ui/src/app/api/bundles/all/route.ts new file mode 100644 index 0000000..847cacf --- /dev/null +++ b/ui/src/app/api/bundles/all/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import { listAllBundleKeys } from "@/lib/s3"; + +export async function GET() { + try { + const bundleKeys = await listAllBundleKeys(); + return NextResponse.json(bundleKeys); + } catch (error) { + console.error("Error fetching all bundle keys:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/ui/src/app/api/bundles/route.ts b/ui/src/app/api/bundles/route.ts new file mode 100644 index 0000000..eb2a349 --- /dev/null +++ b/ui/src/app/api/bundles/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import { db } from "@/db"; +import { bundles } from "@/db/schema"; + +export interface Bundle { + id: string; + txnHashes: string[] | null; +} + +export async function GET() { + try { + const allBundles = await db + .select({ + id: bundles.id, + txnHashes: bundles.txnHashes, + }) + .from(bundles); + + return NextResponse.json(allBundles); + } catch (error) { + console.error("Error fetching bundles:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/ui/src/app/api/txn/[hash]/route.ts b/ui/src/app/api/txn/[hash]/route.ts new file mode 100644 index 0000000..44f6824 --- /dev/null +++ b/ui/src/app/api/txn/[hash]/route.ts @@ -0,0 +1,73 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { + type BundleEvent, + getBundleHistory, + getTransactionMetadataByHash, +} from "@/lib/s3"; + +export interface TransactionEvent { + type: string; + data: { + bundle_id?: string; + transactions?: Array<{ + id: { + sender: string; + nonce: string; + hash: string; + }; + data: string; + }>; + transaction_ids?: Array<{ + sender: string; + nonce: string; + hash: string; + }>; + block_number?: number; + flashblock_index?: number; + block_hash?: string; + }; +} + +export interface TransactionHistoryResponse { + hash: string; + bundle_ids: string[]; + history: BundleEvent[]; +} + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ hash: string }> }, +) { + try { + const { hash } = await params; + + const metadata = await getTransactionMetadataByHash(hash); + + if (!metadata) { + return NextResponse.json( + { error: "Transaction not found" }, + { status: 404 }, + ); + } + + // TODO: Can be in multiple bundles + const bundle = await getBundleHistory(metadata.bundle_ids[0]); + if (!bundle) { + return NextResponse.json({ error: "Bundle not found" }, { status: 404 }); + } + + const response: TransactionHistoryResponse = { + hash, + bundle_ids: metadata.bundle_ids, + history: bundle.history, + }; + + return NextResponse.json(response); + } catch (error) { + console.error("Error fetching transaction data:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/ui/src/app/bundles/[uuid]/page.tsx b/ui/src/app/bundles/[uuid]/page.tsx new file mode 100644 index 0000000..38b4286 --- /dev/null +++ b/ui/src/app/bundles/[uuid]/page.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { BundleHistoryResponse } from "@/app/api/bundle/[uuid]/route"; + +interface PageProps { + params: Promise<{ uuid: string }>; +} + +function formatEventType(eventType: string): string { + switch (eventType) { + case "ReceivedBundle": + return "Bundle Received"; + case "CancelledBundle": + return "Bundle Cancelled"; + case "BuilderMined": + return "Builder Mined"; + case "FlashblockInclusion": + return "Flashblock Inclusion"; + case "BlockInclusion": + return "Block Inclusion"; + default: + return eventType; + } +} + +function getEventStatus(eventType: string): { color: string; bgColor: string } { + switch (eventType) { + case "ReceivedBundle": + return { color: "text-blue-600", bgColor: "bg-blue-100" }; + case "CancelledBundle": + return { color: "text-red-600", bgColor: "bg-red-100" }; + case "BuilderMined": + return { color: "text-yellow-600", bgColor: "bg-yellow-100" }; + case "FlashblockInclusion": + return { color: "text-purple-600", bgColor: "bg-purple-100" }; + case "BlockInclusion": + return { color: "text-green-600", bgColor: "bg-green-100" }; + default: + return { color: "text-gray-600", bgColor: "bg-gray-100" }; + } +} + +export default function BundlePage({ params }: PageProps) { + const [uuid, setUuid] = useState(""); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const initializeParams = async () => { + const resolvedParams = await params; + setUuid(resolvedParams.uuid); + }; + initializeParams(); + }, [params]); + + useEffect(() => { + if (!uuid) return; + + const fetchData = async () => { + try { + const response = await fetch(`/api/bundle/${uuid}`); + if (!response.ok) { + if (response.status === 404) { + setError("Bundle not found"); + } else { + setError("Failed to fetch bundle data"); + } + setData(null); + return; + } + const result = await response.json(); + setData(result); + setError(null); + } catch (_err) { + setError("Failed to fetch bundle data"); + setData(null); + } finally { + setLoading(false); + } + }; + + fetchData(); + + const interval = setInterval(fetchData, 400); + + return () => clearInterval(interval); + }, [uuid]); + + if (!uuid) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Bundle {uuid}

+ {loading && ( +
Loading bundle data...
+ )} + {error && ( +
{error}
+ )} +
+ + {data && ( +
+ {(() => { + const allTransactions = new Set(); + + data.history.forEach((event) => { + if (event.event === "Created") { + event.data?.bundle?.revertingTxHashes?.forEach((tx) => { + allTransactions.add(tx); + }); + } + }); + + const uniqueTransactions = Array.from(allTransactions.values()); + + return uniqueTransactions.length > 0 ? ( +
+

Transactions

+
    + {uniqueTransactions.map((tx) => ( +
  • {tx}
  • + ))} +
+
+ ) : null; + })()} + +
+

Bundle History

+ + {data.history.length > 0 ? ( +
+ {data.history.map((event, index) => { + const { color, bgColor } = getEventStatus(event.event); + return ( +
+
+
+ + {formatEventType(event.event)} + + + {event.data?.timestamp + ? new Date(event.data?.timestamp).toLocaleString() + : "No timestamp"} + +
+ + Event #{index + 1} + +
+
+ ); + })} +
+ ) : ( +

+ {loading + ? "Loading events..." + : "No events found for this bundle."} +

+ )} +
+
+ )} +
+ ); +} diff --git a/ui/src/app/bundles/page.tsx b/ui/src/app/bundles/page.tsx new file mode 100644 index 0000000..2be0c36 --- /dev/null +++ b/ui/src/app/bundles/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import type { Bundle } from "@/app/api/bundles/route"; + +export default function BundlesPage() { + const [liveBundles, setLiveBundles] = useState([]); + const [allBundles, setAllBundles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchLiveBundles = async () => { + try { + const response = await fetch("/api/bundles"); + if (!response.ok) { + setError("Failed to fetch live bundles"); + setLiveBundles([]); + return; + } + const result = await response.json(); + setLiveBundles(result); + setError(null); + } catch (_err) { + setError("Failed to fetch live bundles"); + setLiveBundles([]); + } + }; + + const fetchAllBundles = async () => { + try { + const response = await fetch("/api/bundles/all"); + if (!response.ok) { + console.error("Failed to fetch all bundles from S3"); + setAllBundles([]); + return; + } + const result = await response.json(); + setAllBundles(result); + } catch (_err) { + console.error("Failed to fetch all bundles from S3"); + setAllBundles([]); + } + }; + + const fetchData = async () => { + await Promise.all([fetchLiveBundles(), fetchAllBundles()]); + setLoading(false); + }; + + fetchData(); + + const interval = setInterval(fetchLiveBundles, 400); + + return () => clearInterval(interval); + }, []); + + if (loading) { + return ( +
+

BundleStore (fka Mempool)

+
Loading bundles...
+
+ ); + } + + return ( +
+
+

BundleStore (fka Mempool)

+ {error && ( +
{error}
+ )} +
+ +
+
+

Live Bundles

+ {liveBundles.length > 0 ? ( +
    + {liveBundles.map((bundle) => ( +
  • + + + {bundle.id} + {" ("} + {bundle.txnHashes?.join(", ") || "No transactions"} + {")"} + + +
  • + ))} +
+ ) : ( +

+ No live bundles found. +

+ )} +
+ +
+

All Bundles

+ {allBundles.length > 0 ? ( +
    + {allBundles.map((bundleId) => ( +
  • + + {bundleId} + +
  • + ))} +
+ ) : ( +

+ No bundles found in S3. +

+ )} +
+
+
+ ); +} diff --git a/ui/src/app/globals.css b/ui/src/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/ui/src/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/ui/src/app/layout.tsx b/ui/src/app/layout.tsx new file mode 100644 index 0000000..bc9a9f2 --- /dev/null +++ b/ui/src/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "TIPS", + description: "A beautiful UI for interacting with TIPS", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/ui/src/app/page.tsx b/ui/src/app/page.tsx new file mode 100644 index 0000000..afe1146 --- /dev/null +++ b/ui/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/bundles"); +} diff --git a/ui/src/app/txn/[hash]/page.tsx b/ui/src/app/txn/[hash]/page.tsx new file mode 100644 index 0000000..76f1dda --- /dev/null +++ b/ui/src/app/txn/[hash]/page.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import type { TransactionHistoryResponse } from "@/app/api/txn/[hash]/route"; + +interface PageProps { + params: Promise<{ hash: string }>; +} + +export default function TransactionRedirectPage({ params }: PageProps) { + const router = useRouter(); + const [hash, setHash] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const initializeParams = async () => { + const resolvedParams = await params; + setHash(resolvedParams.hash); + }; + initializeParams(); + }, [params]); + + useEffect(() => { + if (!hash) return; + + const fetchAndRedirect = async () => { + try { + const response = await fetch(`/api/txn/${hash}`); + if (!response.ok) { + if (response.status === 404) { + setError("Transaction not found"); + } else { + setError("Failed to fetch transaction data"); + } + return; + } + const result: TransactionHistoryResponse = await response.json(); + + if (result.bundle_ids && result.bundle_ids.length > 0) { + router.push(`/bundles/${result.bundle_ids[0]}`); + } else { + setError("No bundle found for this transaction"); + } + } catch (_err) { + setError("Failed to fetch transaction data"); + } finally { + setLoading(false); + } + }; + + fetchAndRedirect(); + }, [hash, router]); + + if (!hash) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Transaction {hash}

+ {loading && ( +
+ Redirecting to bundle page... +
+ )} + {error && ( +
{error}
+ )} +
+
+ ); +} diff --git a/ui/src/db/index.ts b/ui/src/db/index.ts new file mode 100644 index 0000000..55c8a8f --- /dev/null +++ b/ui/src/db/index.ts @@ -0,0 +1,9 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import * as schema from "./schema"; + +const pool = new Pool({ + connectionString: process.env.TIPS_DATABASE_URL, +}); + +export const db = drizzle(pool, { schema }); diff --git a/ui/src/db/relations.ts b/ui/src/db/relations.ts new file mode 100644 index 0000000..e69de29 diff --git a/ui/src/db/schema.ts b/ui/src/db/schema.ts new file mode 100644 index 0000000..38c9041 --- /dev/null +++ b/ui/src/db/schema.ts @@ -0,0 +1,33 @@ +import { + bigint, + char, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +export const bundles = pgTable("bundles", { + id: uuid().primaryKey().notNull(), + senders: char({ length: 42 }).array(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + minimumBaseFee: bigint("minimum_base_fee", { mode: "number" }), + txnHashes: char("txn_hashes", { length: 66 }).array(), + txs: text().array().notNull(), + revertingTxHashes: char("reverting_tx_hashes", { length: 66 }).array(), + droppingTxHashes: char("dropping_tx_hashes", { length: 66 }).array(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + blockNumber: bigint("block_number", { mode: "number" }), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + minTimestamp: bigint("min_timestamp", { mode: "number" }), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + maxTimestamp: bigint("max_timestamp", { mode: "number" }), + createdAt: timestamp("created_at", { + withTimezone: true, + mode: "string", + }).notNull(), + updatedAt: timestamp("updated_at", { + withTimezone: true, + mode: "string", + }).notNull(), +}); diff --git a/ui/src/lib/s3.ts b/ui/src/lib/s3.ts new file mode 100644 index 0000000..d79ff9d --- /dev/null +++ b/ui/src/lib/s3.ts @@ -0,0 +1,154 @@ +import { + GetObjectCommand, + ListObjectsV2Command, + S3Client, + type S3ClientConfig, +} from "@aws-sdk/client-s3"; + +function createS3Client(): S3Client { + const configType = process.env.TIPS_UI_S3_CONFIG_TYPE || "aws"; + const region = process.env.TIPS_UI_AWS_REGION || "us-east-1"; + + if (configType === "manual") { + console.log("Using Manual S3 configuration"); + const config: S3ClientConfig = { + region, + forcePathStyle: true, + }; + + if (process.env.TIPS_UI_S3_ENDPOINT) { + config.endpoint = process.env.TIPS_UI_S3_ENDPOINT; + } + + if ( + process.env.TIPS_UI_S3_ACCESS_KEY_ID && + process.env.TIPS_UI_S3_SECRET_ACCESS_KEY + ) { + config.credentials = { + accessKeyId: process.env.TIPS_UI_S3_ACCESS_KEY_ID, + secretAccessKey: process.env.TIPS_UI_S3_SECRET_ACCESS_KEY, + }; + } + + return new S3Client(config); + } + + console.log("Using AWS S3 configuration"); + return new S3Client({ + region, + }); +} + +const s3Client = createS3Client(); + +const BUCKET_NAME = process.env.TIPS_UI_S3_BUCKET_NAME; +if (process.env.TIPS_UI_S3_BUCKET_NAME === undefined) { + throw new Error("You must specify a valid bucket"); +} + +export interface TransactionMetadata { + bundle_ids: string[]; + sender: string; + nonce: string; +} + +async function getObjectContent(key: string): Promise { + try { + const command = new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: key, + }); + + const response = await s3Client.send(command); + const body = await response.Body?.transformToString(); + return body || null; + } catch (error) { + console.error(`Failed to get S3 object ${key}:`, error); + return null; + } +} + +export async function getTransactionMetadataByHash( + hash: string, +): Promise { + const key = `transactions/by_hash/${hash}`; + const content = await getObjectContent(key); + + if (!content) { + return null; + } + + try { + return JSON.parse(content) as TransactionMetadata; + } catch (error) { + console.error( + `Failed to parse transaction metadata for hash ${hash}:`, + error, + ); + return null; + } +} + +export interface BundleEvent { + event: string; + data?: { + bundle?: { + revertingTxHashes: Array; + }; + key: string; + timestamp: number; + }; +} + +export interface BundleHistory { + history: BundleEvent[]; +} + +export async function getBundleHistory( + bundleId: string, +): Promise { + const key = `bundles/${bundleId}`; + const content = await getObjectContent(key); + + if (!content) { + return null; + } + + try { + return JSON.parse(content) as BundleHistory; + } catch (error) { + console.error( + `Failed to parse bundle history for bundle ${bundleId}:`, + error, + ); + return null; + } +} + +export async function listAllBundleKeys(): Promise { + try { + const command = new ListObjectsV2Command({ + Bucket: BUCKET_NAME, + Prefix: "bundles/", + }); + + const response = await s3Client.send(command); + const bundleKeys: string[] = []; + + if (response.Contents) { + for (const object of response.Contents) { + if (object.Key?.startsWith("bundles/")) { + const bundleId = object.Key.replace("bundles/", ""); + if (bundleId) { + bundleKeys.push(bundleId); + } + } + } + } + + return bundleKeys; + } catch (error) { + console.error("Failed to list S3 bundle keys:", error); + return []; + } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..c133409 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}