diff --git a/.claude/commands/context-save.md b/.claude/commands/context-save.md new file mode 100644 index 0000000..6bd79a6 --- /dev/null +++ b/.claude/commands/context-save.md @@ -0,0 +1,14 @@ +Save current session context to MEMORY.md before compaction or heavy work. + +Do the following: +1. Read the current MEMORY.md at `~/.claude/projects/-Users-kumardivyarajat-WebstormProjects-Notiflo/memory/MEMORY.md` +2. Run `git status` and `git log --oneline -5` to capture current state +3. Update MEMORY.md with: + - Current branch and its purpose + - What was built/changed THIS session (list files modified) + - Key decisions made this session + - Any errors encountered and how they were resolved + - What's in progress / next steps + - Any new gotchas discovered +4. Keep MEMORY.md under 200 lines total +5. Confirm what was saved diff --git a/.claude/commands/progress.md b/.claude/commands/progress.md new file mode 100644 index 0000000..f1c537d --- /dev/null +++ b/.claude/commands/progress.md @@ -0,0 +1,13 @@ +Show current project progress and update MEMORY.md. + +Do the following: +1. Read MEMORY.md to understand last known state +2. Run `git status` and `git log --oneline -10` +3. Check if there's an active plan file in `~/.claude/plans/` +4. Summarize: + - Current branch and uncommitted changes + - What's been built (from git log + MEMORY.md) + - What's pending / in progress + - Any active plan and its status +5. Update MEMORY.md with the current state +6. Present the summary to the user diff --git a/.claude/commands/recover-context.md b/.claude/commands/recover-context.md new file mode 100644 index 0000000..667234c --- /dev/null +++ b/.claude/commands/recover-context.md @@ -0,0 +1,13 @@ +Recover full context after a session restart or context loss. + +Do the following: +1. Read CLAUDE.md (project root) for stable project truth +2. Read MEMORY.md for last known session state +3. Run `git status` and `git log --oneline -15` to see what actually happened +4. If MEMORY.md seems stale or incomplete: + a. Find JSONL transcript files: `ls -lt ~/.claude/projects/-Users-kumardivyarajat-WebstormProjects-Notiflo/*.jsonl` + b. Read the most recent transcript to extract user messages and key decisions + c. Update MEMORY.md with recovered context +5. Check for active plan files in `~/.claude/plans/` +6. Present a summary of: where we are, what's done, what's next +7. Do NOT ask the user to re-explain anything — recover it from the files diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 0000000..081aa9b --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,13 @@ +Run the full test suite across all projects and report results. + +Do the following: +1. Run all three test suites in parallel: + - `npx nx test notiflo` + - `npx nx test napi-bridge` + - `npx nx test pipeline-pipeline` +2. Report results in this format: + - notiflo: X passing, Y failing + - napi-bridge: X passing, Y failing + - pipeline: X passing, Y failing +3. If there are NEW failures (not pre-existing), flag them prominently +4. If all tests pass (excluding known pre-existing failures), confirm the system is healthy diff --git a/.claude/rules/bridge.md b/.claude/rules/bridge.md new file mode 100644 index 0000000..04f7abc --- /dev/null +++ b/.claude/rules/bridge.md @@ -0,0 +1,34 @@ +--- +paths: + - "libs/bridge/**" +--- + +# Rust-JS Bridge Rules + +## Architecture +- `EngineBridgeService` — wraps the real Rust napi addon (`require('engine-core')`) +- `MockEngineBridgeService` — pure TypeScript implementation for testing +- `IEngineBridge` — interface contract both implement +- `ENGINE_BRIDGE` — DI token string constant + +## Data Flow: Rust <-> Node.js +- NestJS calls napi functions via `EngineBridgeService` +- Data crosses the boundary as JSON strings (serialized in TS, deserialized in Rust) +- Rust match callbacks use `ThreadsafeFunction` to emit events back to NestJS `EventEmitter2` +- Event name: `engine.condition.match` + +## Module Setup +`NapiBridgeModule` is `@Global()` — available everywhere without explicit imports. +Provides both `EngineBridgeService` (class) and `ENGINE_BRIDGE` (string token). + +## Testing Bridge Changes +1. Write tests using `MockEngineBridgeService` first +2. Verify mock behavior matches expected Rust behavior +3. Only test with real addon when specifically testing napi interop +4. Bridge tests: `npx nx test napi-bridge` + +## When Modifying the Bridge Interface +If you change `IEngineBridge`, you MUST update BOTH: +- `EngineBridgeService` (real addon wrapper) +- `MockEngineBridgeService` (test mock) +- All barrel exports in `src/index.ts` diff --git a/.claude/rules/nestjs-patterns.md b/.claude/rules/nestjs-patterns.md new file mode 100644 index 0000000..15b7beb --- /dev/null +++ b/.claude/rules/nestjs-patterns.md @@ -0,0 +1,53 @@ +--- +paths: + - "apps/notiflo/**/*.ts" + - "!apps/notiflo/**/*.spec.ts" +--- + +# NestJS Service & Module Rules + +## Module Pattern +Every module that exports a service MUST provide both class and string token: +```typescript +@Module({ + providers: [ + MyService, + { provide: 'MyService', useExisting: MyService }, + ], + exports: [MyService, 'MyService'], +}) +``` +Forgetting the string token alias causes "can't resolve dependencies" errors in consuming modules. + +## Service Pattern +```typescript +@Injectable() +export class MyService implements OnModuleInit { + private readonly logger = new Logger(MyService.name); + + async onModuleInit() { /* startup logic */ } +} +``` + +## Controller Pattern +- `@Controller('feature-name')` for route prefix +- Return objects directly — NestJS serializes to JSON +- Use `class-validator` decorators on DTOs +- `@UsePipes(new ValidationPipe({ transform: true }))` or global pipe + +## Mongoose Model Registration +**Critical:** The name in `@InjectModel('X')` must EXACTLY match `MongooseModule.forFeature([{ name: 'X', schema }])`. +This has caused real bugs. Always verify both files when creating or modifying a schema. + +## Dependency Injection +- Use `@Optional()` with `@Inject(TOKEN)` when a dependency might not exist +- Use `forwardRef(() => Module)` for circular module dependencies +- The `ENGINE_BRIDGE` token uses `@Optional()` so the app works without the Rust addon + +## Scaffolding — Use NX CLI +NEVER manually create modules, services, or controllers. Always: +```bash +npx nx generate @nx/nest:resource feature-name --project=notiflo +npx nx generate @nx/nest:service service-name --project=notiflo +npx nx generate @nx/nest:module module-name --project=notiflo +``` diff --git a/.claude/rules/pipeline.md b/.claude/rules/pipeline.md new file mode 100644 index 0000000..068f317 --- /dev/null +++ b/.claude/rules/pipeline.md @@ -0,0 +1,44 @@ +--- +paths: + - "libs/pipeline/**" +--- + +# Pipeline Library Rules + +## Architecture +The pipeline processes notifications through 4 sequential stages: +1. **Fanout** — resolves subscribers, fans out to channels +2. **Render** — resolves templates, renders with Handlebars per channel +3. **Deliver** — sends through channel providers with resilience patterns +4. **Status** — tracks delivery status, updates notification records + +Kafka connects the stages. Each worker consumes from one topic and produces to the next. + +## Worker Pattern +All workers implement `IWorker`: +```typescript +interface IWorker { + start(): Promise; + stop(): Promise; + isRunning(): boolean; + getMetrics(): WorkerMetrics; +} +``` +Workers track: `processedCount`, `failedCount`, `totalLatencyMs`, `lastProcessedAt`. + +## Resilience Patterns +- `CircuitBreaker` — CLOSED -> OPEN (after failures) -> HALF_OPEN (after timeout) -> CLOSED (on success) +- `RateLimiter` — token bucket with configurable refill +- `RetryHandler` — exponential backoff with jitter, configurable retryable errors +- `BatchAccumulator` — accumulates messages, flushes at size threshold or time interval +- `DeadLetterQueue` — failed messages after all retries exhausted + +## Delivery Worker Specifics +- Per-channel batch accumulators +- Redis-based deduplication with TTL +- Provider registry: `registerProvider(channel, IChannelDeliveryProvider)` +- Publishes status messages after delivery (success or failure) + +## Kafka is OFF the Hot Path +Kafka is for durability, replay, and analytics ONLY. The real-time alert path goes: +tick -> Rust engine -> EventEmitter -> AlertDeliveryListener -> OrchestratorService -> channel provider diff --git a/.claude/rules/rust-engine.md b/.claude/rules/rust-engine.md new file mode 100644 index 0000000..d3b366e --- /dev/null +++ b/.claude/rules/rust-engine.md @@ -0,0 +1,44 @@ +--- +paths: + - "libs/engine/**/*.rs" + - "libs/engine/**/Cargo.toml" + - "Cargo.toml" +--- + +# Rust Engine Rules + +## Architecture +- `engine-core` is a cdylib (Node.js addon via napi-rs) AND rlib (for Rust tests/benches) +- `shared-types` is an rlib defining the `EvaluationStrategy` trait and domain types +- The hot path target is <1us per no-match evaluation, 2-6ms tick-to-delivery + +## Conventions +- Gate all napi exports behind `#[cfg(feature = "napi_binding")]` in `napi_exports.rs` +- Never put napi dependencies in the default feature set — benchmarks must compile without Node.js symbols +- Use `DashMap` for concurrent condition storage, `crossbeam-channel` for ring buffers +- `parking_lot` mutexes over `std::sync` for performance +- `Option` in napi structs means JS must pass `undefined`, NOT `null` + +## Adding a New Evaluation Strategy +1. Implement `EvaluationStrategy` trait from `shared-types/src/strategy.rs` +2. Add strategy type variant to the strategy enum +3. Register in the evaluator dispatcher (`engine-core/src/condition/evaluator.rs`) +4. Write Rust unit tests in the same file +5. Add benchmark in `benches/condition_bench.rs` +6. Add corresponding handling in `MockEngineBridgeService` (TypeScript side) +7. Write bridge tests in `libs/bridge/napi-bridge/` + +## Build & Test +```bash +cargo check --workspace # Fast compilation check +cargo test -p engine-core # Unit tests +cargo test -p shared-types # Shared type tests +cargo clippy --workspace # Lint +cargo bench --bench condition_bench --no-default-features # Benchmarks +npx nx build engine-core # Build cdylib for Node.js +``` + +## The .node Addon +- Built artifact: `target/release/libengine_core.dylib` +- Must be copied/symlinked to `engine-core.darwin-arm64.node` for `require('engine-core')` to work +- For ALL TypeScript tests, use `MockEngineBridgeService` instead — never depend on the compiled addon in Jest diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..1ebad8b --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,59 @@ +--- +paths: + - "**/*.spec.ts" + - "**/*.e2e.spec.ts" + - "**/test-utils/**" +--- + +# Testing Rules + +## TDD Is Non-Negotiable +- Write ALL test expectations FIRST — they must FAIL (RED) +- Then implement minimum code to pass (GREEN) +- Then refactor if needed +- Tests are the source of truth for the entire platform + +## Writing Non-Tautological Tests +- Don't just test that a mock was called — test it was called with the RIGHT arguments +- Test error paths, not just happy paths +- Test state transitions (e.g., campaign DRAFT -> RUNNING -> PAUSED) +- Test edge cases: empty arrays, null values, missing fields +- Integration tests should verify the full event chain works + +## Unit Tests (per service/controller) +- Mock ALL dependencies with `jest.fn()` objects +- For Mongoose models: + ```typescript + const mockModel = { + create: jest.fn(), + find: jest.fn().mockReturnValue({ + skip: jest.fn().mockReturnValue({ + limit: jest.fn().mockReturnValue({ exec: jest.fn() }) + }) + }), + findById: jest.fn().mockReturnValue({ exec: jest.fn() }), + }; + ``` +- Provide mock with `getModelToken('ModelName')` in test module +- For string DI tokens: `{ provide: 'ServiceName', useValue: mockService }` + +## Integration Tests +- Use real `EventEmitter2`, `MockEngineBridgeService`, mocked DB +- Test the feature flow end-to-end within a module + +## E2E Tests +- Use `MongoMemoryServer` for real MongoDB +- Set `process.env.MONGODB_URI` BEFORE module compilation +- Override engine bridge: `.overrideProvider(EngineBridgeService).useClass(MockEngineBridgeService)` +- Use `import request from 'supertest'` (default import, NOT namespace `import *`) +- After any significant code change, run: `npx nx run-many --target=test --all` + +## MockEngineBridgeService +- Pure TypeScript implementation of `IEngineBridge` +- Used in ALL Jest tests — never depend on compiled Rust addon +- Mirrors Rust engine behavior (threshold evaluation, match events) +- Injected via `ENGINE_BRIDGE` token with `@Optional()` + +## Test Naming +- Use: `'should [action] when [condition]'` +- Group with `describe()` blocks by feature/method diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..31a6a87 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# ============================================================================= +# Notiflo — Infrastructure Configuration +# ============================================================================= +# Copy this file to .env and adjust for your environment. +# +# NOTE: Provider credentials (SendGrid, Twilio, FCM, etc.) are NOT configured +# here. They are per-organization settings managed through the UI/API/MCP/CLI +# and stored in the database. + +# ── Core ───────────────────────────────────────────────────────────────────── +PORT=3000 +CORS_ORIGIN=* + +# ── Database ───────────────────────────────────────────────────────────────── +MONGODB_URI=mongodb://localhost/notiflo + +# ── MCP Server ─────────────────────────────────────────────────────────────── +NOTIFLO_API_URL=http://localhost:3000/api + +# ── Redis ──────────────────────────────────────────────────────────────────── +REDIS_URL=redis://localhost:6379 + +# ── Frontend ───────────────────────────────────────────────────────────────── +NEXT_PUBLIC_API_URL=/api diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..207fdb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug Report +about: Report a bug in Notiflo +title: "[Bug] " +labels: bug +assignees: '' +--- + +**Component** +- [ ] Rust runtime (`notiflo-runtime`) +- [ ] NestJS API (`notiflo-api`) +- [ ] Docker / Infrastructure +- [ ] Documentation + +**Describe the bug** +A clear description of what the bug is. + +**Steps to reproduce** +1. +2. +3. + +**Expected behavior** +What you expected to happen. + +**Actual behavior** +What actually happened. Include error messages or logs if available. + +**Environment** +- OS: +- Rust version (if applicable): +- Node.js version (if applicable): +- Docker version (if applicable): +- Notiflo version: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..44267fc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement +title: "[Feature] " +labels: enhancement +assignees: '' +--- + +**Is this related to a problem?** +A clear description of the problem or limitation. + +**Proposed solution** +Describe the feature or change you'd like. + +**Alternatives considered** +Any alternative approaches you've considered. + +**Additional context** +Any other context, mockups, or references. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ff1ced9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## Summary + + + +## Changes + +- + +## Testing + +- [ ] `cargo test --workspace --no-default-features` +- [ ] `cargo clippy --workspace --no-default-features -- -D warnings` +- [ ] `npx nx test notiflo` +- [ ] New tests added for new functionality + +## Related Issues + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49b49b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,133 @@ +name: CI +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + rust: + runs-on: ubuntu-latest + services: + mongodb: + image: mongo:7 + ports: + - 27017:27017 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: rust-ci + - run: cargo test --workspace --no-default-features + - run: cargo clippy --workspace --no-default-features -- -D warnings + - run: cargo build --release --bin notiflo-runtime --no-default-features + + rust-integration: + runs-on: ubuntu-latest + services: + mongodb: + image: mongo:7 + ports: + - 27017:27017 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: rust-ci + - run: cargo test --tests -p notiflo-runtime --no-default-features --features integration-tests + env: + NOTIFLO_MONGODB_URI: mongodb://localhost:27017 + NOTIFLO_REDIS_URL: redis://localhost:6379 + + node: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn + - run: yarn install --frozen-lockfile + - run: npx nx test notiflo --passWithNoTests + + nestjs-e2e: + runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn + - run: yarn install --frozen-lockfile + - run: npx nx e2e notiflo-e2e + env: + REDIS_URL: redis://localhost:6379 + MONGOMS_VERSION: 8.0.4 + + load-test-smoke: + runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: rust-ci + - name: Run hot-path load test (smoke) + run: cargo run --release --bin load-test --no-default-features -- --conditions 1000 --ticks 10000 --redis-url redis://localhost:6379 + + bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: rust-ci + + - name: Run pipeline benchmarks + run: cargo bench --bench pipeline_bench --no-default-features -- --output-format bencher 2>&1 | tee bench_output.txt + + - name: Run condition benchmarks + run: cargo bench --bench condition_bench --no-default-features -- --output-format bencher 2>&1 | tee -a bench_output.txt + + - name: Check hot-path regression + run: | + # Extract evaluate_throughput/threshold_only/1000 result (ns/iter) + EVAL_NS=$(grep 'evaluate_throughput/threshold_only/1000' bench_output.txt | head -1 | awk '{print $5}') + echo "Evaluate latency (1K conditions): ${EVAL_NS} ns/iter" + + # Fail if evaluate latency exceeds 500ns (>5x regression from ~75ns baseline) + # CI runners are slower than local machines, so we use a generous threshold + if [ -n "$EVAL_NS" ]; then + THRESHOLD=500 + EVAL_INT=${EVAL_NS%.*} + if [ "$EVAL_INT" -gt "$THRESHOLD" ]; then + echo "REGRESSION DETECTED: evaluate latency ${EVAL_NS}ns exceeds ${THRESHOLD}ns threshold" + exit 1 + fi + echo "OK: evaluate latency ${EVAL_NS}ns within ${THRESHOLD}ns threshold" + else + echo "WARNING: Could not parse benchmark output, skipping regression check" + fi diff --git a/.gitignore b/.gitignore index 6c41dff..1ba073d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,14 +34,33 @@ yarn-error.log testem.log /typings +# Environment variables +.env +.env.local +.env.*.local + # System Files .DS_Store Thumbs.db .nx/cache +# Rust build artifacts +/target +*.node + # Downloaded binaries *.tgz # Lock files (using yarn.lock) -package-lock.json \ No newline at end of file +package-lock.json + +# Claude Code personal files (not shared) +CLAUDE.local.md +.claude/settings.local.json + +# Next.js +.next + +# Criterion benchmark reports +libs/engine/notiflo-runtime/target/criterion/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1490c9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [0.1.0] - 2026-02-20 + +### Added + +- Rust runtime (`notiflo-runtime`) with full hot-path pipeline: ingest, evaluate, template render, HTTP deliver, Redis Streams event log +- Drift Sentinel algorithm for O(1) amortized threshold crossing evaluation (~75ns per tick) +- Three pluggable evaluation strategies: `threshold_crossing`, `expression` DSL, `script` (Rhai sandbox) +- Multi-channel delivery: email (SendGrid/SMTP), SMS (Twilio), push (FCM/APNs), webhook, Slack, WhatsApp, in-app +- NestJS control plane API for managing organizations, subscribers, alerts, templates, and channels +- Redis Streams consumer for delivery event tracking and notification status updates +- Dashboard API endpoints for analytics and monitoring +- MCP (Model Context Protocol) stdio server for AI-native alert management +- Next.js dashboard UI with real-time metrics +- Docker Compose setup (MongoDB 7 + Redis 7 + API + Runtime) +- Criterion benchmarks for pipeline throughput, template rendering, and HTTP delivery +- Built-in Rust load test binary (`load-test`) for sustained hot-path testing +- NestJS E2E tests against real MongoDB (MongoMemoryServer) and Redis +- Rust integration tests against real MongoDB and Redis +- CI pipeline: Rust tests + clippy, integration tests, NestJS unit/E2E tests, load test smoke run diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..66d11e2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,165 @@ +# Notiflo — Claude Code Instructions + +## What Is Notiflo + +Notiflo is a **real-time alerting pipeline**: stream ingestion → condition evaluation → multi-channel delivery. The entire product is speed. Every nanosecond matters. + +The core pipeline runs in Rust as a standalone binary. NestJS is the control plane (config API, dashboard). The hot path never leaves Rust. + +### Architecture + +``` +[Data Sources] → [Rust Pipeline Worker] → [Provider APIs] + WebSocket Ingest → Evaluate SendGrid + Kafka (Drift Sentinel) Twilio + Redis Queue → Deliver FCM + → Event Log OneSignal + +[NestJS Control Plane] ← Redis Streams (delivery events) + Config API (alerts, subscribers, templates, channels) + Dashboard API + MongoDB (persistence) + +[Next.js Dashboard] + Overview, alerts, notifications, engine status +``` + +**Rust binary (the worker):** Consumes streams, evaluates conditions via Drift Sentinel, delivers to providers, writes delivery events to Redis streams. + +**NestJS (the server):** REST API for config CRUD, dashboard endpoints, reads delivery events from Redis streams for history/observability. + +**They are two separate processes.** NestJS talks to users. Rust talks to data. + +### Drift Sentinel Algorithm + +The default threshold evaluation algorithm. Key insight: after a crossing, the new sentinel position is found by walking the sorted array from the current index — no binary search needed. O(1) amortized sentinel updates. + +Based on: "A stochastic cost model for streaming threshold evaluation under local continuity." + +3-10x faster than binary search in the local-continuity (realistic) scenario. Binary search is available as an alternative via `ThresholdAlgorithm::BinarySearch`. + +### Pluggable Evaluation Strategies + +1. `threshold_crossing` — Drift Sentinel (default) or Binary Search +2. `expression` — DSL parser for compound conditions +3. `script` — Rhai sandboxed scripting + +New strategies implement the `EvaluationStrategy` trait and register in `evaluator.rs`. + +--- + +## Project Structure + +``` +/ +├── apps/ +│ ├── notiflo/ # NestJS control plane +│ │ └── src/app/ +│ │ ├── alerts/ # Alert conditions CRUD + tick ingestion +│ │ ├── channels/ # Channel providers + registry +│ │ ├── core/types/ # Channel + notification type definitions +│ │ ├── dashboard/ # Dashboard API + engine metrics +│ │ ├── notifications/ # Notification records +│ │ ├── organizations/ # Multi-tenant org management +│ │ ├── subscribers/ # Subscriber management +│ │ └── templates/ # Template engine (Handlebars) +│ └── notiflo-web/ # Next.js dashboard (as-is) +├── libs/ +│ ├── bridge/napi-bridge/ # @notiflo/bridge/napi-bridge — Rust addon wrapper + mock +│ └── engine/ +│ ├── engine-core/ # Rust — condition evaluation engine +│ │ ├── src/condition/ +│ │ │ ├── threshold_crossing.rs # Drift Sentinel + Binary Search +│ │ │ ├── evaluator.rs # Strategy registry + dispatch +│ │ │ ├── expression_strategy.rs # DSL evaluator +│ │ │ └── script_strategy.rs # Rhai sandbox +│ │ ├── benches/condition_bench.rs # Criterion benchmarks +│ │ └── src/napi_exports.rs # Node.js bridge (for config push) +│ └── shared-types/ # Rust shared types (traits, tick, condition) +├── Cargo.toml # Rust workspace root +└── tsconfig.base.json # TS path aliases +``` + +### Path Aliases +``` +@notiflo/bridge/napi-bridge → libs/bridge/napi-bridge/src/index.ts +engine-core → libs/engine/engine-core/index.d.ts +``` + +--- + +## Build & Test Commands + +### Rust +```bash +export PATH="$HOME/.cargo/bin:$PATH" +cargo check --workspace # Fast compilation check +cargo test --workspace # Run all Rust tests +cargo bench --bench condition_bench --no-default-features # Benchmarks +npx nx build engine-core # Build napi binary +``` + +### NestJS +```bash +npx nx test notiflo # Backend tests +npx nx test notiflo --testPathPattern="alerts" # Specific module +npx nx serve notiflo # Start server +``` + +### Frontend +```bash +npx nx test notiflo-web # Frontend tests +npx nx serve notiflo-web # Start dashboard +``` + +--- + +## Key Patterns + +### NestJS +- `@Injectable()` services, `@Optional() @Inject(TOKEN)` for graceful degradation +- String DI tokens: `ENGINE_BRIDGE`, `'AlertsService'` +- Mongoose: `@InjectModel('Name')` must match `forFeature({ name: 'Name' })` +- `MockEngineBridgeService` replaces Rust addon in ALL TS tests + +### Rust +- Feature flag `napi_binding` gates NAPI code +- `EvaluationStrategy` trait — all strategies implement this +- `ThresholdCrossingStrategy::with_algorithm(ThresholdAlgorithm::DriftSentinel)` +- `engine-core` compiles as `cdylib` (Node.js) + `rlib` (Rust tests/benches) + +### Testing +- Unit: Jest + `@nestjs/testing`, mock Mongoose models +- E2E: MongoMemoryServer + MockEngineBridgeService +- Rust: `cargo test --workspace` + +--- + +## What Needs to Be Built (Core Pipeline) + +### Rust standalone binary (`notiflo-runtime`) +1. **Ingest connectors** — WebSocket (tokio-tungstenite), Kafka (rdkafka), Redis queue (redis crate) +2. **Delivery layer** — async HTTP client (reqwest) for provider APIs, retry with backoff, dead letter to Redis stream +3. **Event log** — writes delivery events to Redis streams for NestJS to consume +4. **Config loader** — reads alert conditions from MongoDB or Redis, watches for changes + +### NestJS changes +- Config push: write alert configs to Redis/MongoDB for Rust binary to read +- Dashboard: consume delivery events from Redis streams + +### Design Principles +- Hot path stays in Rust — never crosses to Node +- Eventually consistent but deterministic and fault tolerant +- Delivery events written to durable Redis streams +- Node consumes at its own pace, persists to MongoDB +- Burst-heavy, high-sleep pattern — optimize for the burst + +--- + +## User Preferences + +- **Think before coding.** Never jump to implementation. +- **Speed is the product.** Every architectural decision evaluated against latency. +- **No over-engineering.** Only build what's needed now. +- **Commit only when asked.** +- **Don't ask user to re-explain.** Read CLAUDE.md, MEMORY.md, git log. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b99a718 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,52 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery and unwelcome sexual attention +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainers at **conduct@notiflo.dev**. All complaints +will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..aff350a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,160 @@ +# Contributing to Notiflo + +Thank you for your interest in contributing to Notiflo. This guide covers the project structure, how to run tests, and how to add new evaluation strategies or delivery channels. + +## Prerequisites + +- **Rust 1.78+** -- for the runtime engine +- **Node.js 20+** and **Yarn** -- for the NestJS API +- **MongoDB 7+** -- primary data store +- **Redis 7+** -- queue, pub/sub, and event streams + +## Getting Started + +```bash +# Clone the repository +git clone https://github.com/rajatady/Notiflo.git +cd Notiflo + +# Start infrastructure +docker compose up -d mongodb redis + +# Build and run the Rust runtime +NOTIFLO_MONGODB_URI=mongodb://localhost:27017/notiflo \ +NOTIFLO_REDIS_URL=redis://localhost:6379 \ + cargo run --release --bin notiflo-runtime + +# Install Node.js dependencies and run the API +yarn install +npx nx serve notiflo +``` + +## Code Structure + +``` +apps/ + notiflo/ NestJS API (control plane) + notiflo-e2e/ E2E tests (real MongoDB + Redis) + +libs/engine/ + shared-types/ Rust shared types crate + engine-core/ Condition evaluation (DashMap store, strategies) + src/ + condition/ Evaluation strategies (threshold, expression, script) + delivery/ Channel delivery traits and implementations + feed/ Event log and activity feed + router/ Routing logic (subscriber preferences, fan-out) + cache/ Caching layer + resilience/ Retry and circuit breaker logic + benches/ + condition_bench.rs Criterion benchmarks for evaluation strategies + notiflo-runtime/ Runtime binary + library + src/ + main.rs Entry point + pipeline.rs Ingest + evaluate loops + delivery/ HTTP delivery, routing, retry, dead letter + event_log.rs Redis Streams event log + template.rs Handlebars template renderer + config_loader.rs MongoDB config sync + ingest/ Redis and WebSocket ingestion + benches/ + pipeline_bench.rs Criterion benchmarks (throughput, render, delivery) + tests/ + integration/ Real MongoDB + Redis integration tests +``` + +## Running Tests + +```bash +# Rust unit tests (no external services required) +cargo test --workspace --no-default-features + +# Rust integration tests (requires running MongoDB + Redis) +cargo test --tests -p notiflo-runtime --no-default-features --features integration-tests + +# Rust linting +cargo clippy --workspace -- -D warnings + +# Rust formatting +cargo fmt --all -- --check + +# NestJS unit tests +npx nx test notiflo + +# NestJS E2E tests (requires running Redis) +npx nx e2e notiflo-e2e +``` + +## Running Benchmarks + +```bash +# Pipeline benchmarks (throughput, template render, delivery) +cargo bench --bench pipeline_bench --no-default-features + +# Condition evaluation benchmarks (Drift Sentinel scaling, expression, script) +cargo bench --bench condition_bench --no-default-features +``` + +## Load Testing + +The runtime ships with a built-in load test binary that exercises the full pipeline against real infrastructure. + +```bash +cargo run --release --bin load-test --no-default-features -- \ + --conditions 10000 \ + --ticks 50000 +``` + +Adjust `--conditions` and `--ticks` to match your target workload. The binary prints throughput, latency percentiles, and evaluation rates. + +## Pull Request Process + +1. Fork the repository and create a feature branch from `main`. +2. Write tests for any new functionality. +3. Ensure all checks pass: + - `cargo test --workspace --no-default-features` + - `cargo clippy --workspace -- -D warnings` + - `cargo fmt --all -- --check` + - `npx nx test notiflo` +4. Open a pull request with a clear description of the change. +5. A maintainer will review and merge once CI is green. + +## Adding an Evaluation Strategy + +Evaluation strategies live in `libs/engine/engine-core/src/condition/`. To add a new one: + +1. Create a new module file (e.g., `my_strategy.rs`) in the `condition/` directory. +2. Implement the `EvaluationStrategy` trait: + ```rust + pub trait EvaluationStrategy { + fn evaluate(&mut self, tick: &Tick, params: &StrategyParams) -> EvalResult; + } + ``` +3. Register the strategy in `condition/mod.rs` so the router can dispatch to it by name. +4. Add unit tests in the same file or a dedicated test module. +5. If performance-sensitive, add a benchmark case in `libs/engine/engine-core/benches/condition_bench.rs`. + +## Adding a Delivery Channel + +Delivery channels live in `libs/engine/engine-core/src/delivery/`. To add a new one: + +1. Create a new module file (e.g., `my_channel.rs`) in the `delivery/` directory. +2. Implement the `DeliveryChannel` trait: + ```rust + #[async_trait] + pub trait DeliveryChannel { + async fn deliver(&self, notification: &Notification) -> DeliveryResult; + } + ``` +3. Register the channel in `delivery/mod.rs`. +4. Add the channel name to the `Channel` enum so it can be referenced in alert configurations. +5. Add integration tests that verify delivery against a mock or sandbox endpoint. + +## Code Style + +- **Rust**: Follow standard `rustfmt` formatting. Run `cargo fmt` before committing. +- **TypeScript**: Follow the existing ESLint configuration. Run `npx nx lint notiflo` to check. + +## Questions? + +Open an issue on [GitHub](https://github.com/rajatady/Notiflo/issues) and we will be happy to help. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ef93467 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4030 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap", + "js-sys", + "once_cell", + "rand 0.9.2", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "engine-core" +version = "0.1.0" +dependencies = [ + "criterion", + "crossbeam-channel", + "dashmap", + "napi", + "napi-build", + "napi-derive", + "parking_lot", + "rhai", + "serde", + "serde_json", + "shared-types", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "handlebars" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3f9296c208515b87bd915a2f5d1163d4b3f863ba83337d7713cf478055948e" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.5+1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" + +[[package]] +name = "mongodb" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" +dependencies = [ + "base64", + "bitflags", + "bson", + "derive-where", + "derive_more", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-proto", + "hickory-resolver", + "hmac", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand 0.9.2", + "rustc_version_runtime", + "rustls", + "rustversion", + "serde", + "serde_bytes", + "serde_with", + "sha1", + "sha2", + "socket2 0.6.2", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case 0.6.0", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case 0.6.0", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" +dependencies = [ + "spin", +] + +[[package]] +name = "notiflo-runtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bson", + "clap", + "criterion", + "crossbeam-channel", + "dashmap", + "engine-core", + "futures-util", + "handlebars", + "mongodb", + "parking_lot", + "redis", + "reqwest", + "serde", + "serde_json", + "shared-types", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "uuid", + "wiremock", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redis" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itertools 0.13.0", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rhai" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f9ef5dabe4c0b43d8f1187dc6beb67b53fe607fff7e30c5eb7f71b814b8c2c1" +dependencies = [ + "ahash", + "bitflags", + "no-std-compat", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", + "web-time", +] + +[[package]] +name = "rhai_codegen" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-types" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..61ea70a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = [ + "libs/engine/shared-types", + "libs/engine/engine-core", + "libs/engine/notiflo-runtime", +] +resolver = "2" + +[workspace.dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/Dockerfile b/Dockerfile.api similarity index 85% rename from Dockerfile rename to Dockerfile.api index 9be7426..599adda 100644 --- a/Dockerfile +++ b/Dockerfile.api @@ -1,4 +1,4 @@ -FROM node:18-alpine As development +FROM node:20-alpine As development RUN apk update && apk add yarn curl bash @@ -14,7 +14,7 @@ RUN yarn build notiflo --prod -FROM node:18-alpine as production +FROM node:20-alpine as production WORKDIR /usr/src/app diff --git a/Dockerfile.runtime b/Dockerfile.runtime new file mode 100644 index 0000000..dd22f79 --- /dev/null +++ b/Dockerfile.runtime @@ -0,0 +1,11 @@ +FROM rust:1.78-slim-bookworm AS build +RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY libs/engine/ libs/engine/ +RUN cargo build --release --bin notiflo-runtime + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* +COPY --from=build /app/target/release/notiflo-runtime /usr/local/bin/ +ENTRYPOINT ["notiflo-runtime"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5b80558 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please also get an + "Alarm" or "alarm" (depending on the alarm's sensitivity) + from your clock before submitting. + + Copyright 2024 Notiflo Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index cb85c89..c4e25b2 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,226 @@ # Notiflo - +Real-time alerting pipeline. Stream ingestion, sub-100ns condition evaluation, multi-channel delivery. -✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨ +[![CI](https://github.com/rajatady/Notiflo/actions/workflows/ci.yml/badge.svg)](https://github.com/rajatady/Notiflo/actions/workflows/ci.yml) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![Rust](https://img.shields.io/badge/rust-1.78%2B-orange.svg)](https://www.rust-lang.org/) +## Architecture -## Start the app +``` + +-------------------------------+ + [Data Sources] | notiflo-runtime (Rust) | [Providers] + | | + Redis Queue ---------> | Ingest | + WebSocket -----------> | | | + | v | + | Evaluate | + | Drift Sentinel (<100ns) | + | Expression DSL | + | Rhai Script Sandbox | + | | | + | v | + | Template Render (Handlebars) | + | | | + | v | + | Deliver (HTTP) ---------------+-----> SendGrid (email) + | | |-----> Twilio (sms) + | v |-----> FCM / APNs (push) + | Event Log ----> Redis Streams |-----> Slack, Webhook + +-------------------------------+-----> WhatsApp, In-App + | + v + +-------------------------------+ + | notiflo-api (NestJS) | + | | + | REST API (alerts, templates, | + | subscribers, channels) | + | Dashboard endpoints | + | Delivery event tracking | <--- Redis Streams + +-------------------------------+ + | + v + +---------------+ + | MongoDB 7+ | + +---------------+ +``` -To start the development server run `nx serve notiflo`. Open your browser and navigate to http://localhost:4200/. Happy coding! +**notiflo-runtime** (Rust) -- The hot path. Ingests data streams (Redis queue or WebSocket), evaluates conditions using the Drift Sentinel algorithm in <100ns per tick, renders templates with Handlebars, delivers notifications via HTTP, and logs events to Redis Streams. +**notiflo-api** (NestJS) -- Control plane only. REST API for managing alerts, templates, subscribers, and channels. Dashboard endpoints. Consumes Redis Streams for delivery event tracking. -## Generate code +## Performance -If you happen to use Nx plugins, you can leverage code generators that might come with it. +Benchmarked on a single thread. Drift Sentinel achieves flat O(1) scaling regardless of condition count. -Run `nx list` to get a list of available plugins and whether they have generators. Then run `nx list ` to see what generators are available. +| Metric | Value | Notes | +|--------|-------|-------| +| Throughput | 7.19M ticks/sec | 10K conditions, 100 symbols | +| Evaluate latency (1K conditions) | 75ns avg | threshold_crossing, Drift Sentinel | +| Evaluate latency (100K conditions) | 73ns avg | Scales flat -- O(1) amortized | +| Evaluations/sec (100K conditions) | 13.6M | Single thread | +| Template render | 1.08us | Handlebars | +| HTTP delivery | ~52us | Per notification | -Learn more about [Nx generators on the docs](https://nx.dev/plugin-features/use-code-generators). +## Quick Start -## Running tasks +```bash +docker compose up +``` -To execute tasks with Nx use the following syntax: +The NestJS API will be available at http://localhost:3000. -``` -nx <...options> -``` +### Create Your First Alert + +```bash +# Create an organization +curl -X POST http://localhost:3000/organizations \ + -H 'Content-Type: application/json' \ + -d '{"name": "My Org", "slug": "my-org"}' -You can also run multiple targets: +# Create a subscriber +curl -X POST http://localhost:3000/subscribers \ + -H 'Content-Type: application/json' \ + -d '{"organizationId": "", "externalId": "user-1", "email": "user@example.com", "channelPreferences": {"email": {"enabled": true}}}' +# Create an alert condition +curl -X POST http://localhost:3000/alerts \ + -H 'Content-Type: application/json' \ + -d '{"organizationId": "", "subscriberId": "", "symbol": "AAPL", "strategyType": "threshold_crossing", "strategyParams": {"threshold": 150, "operator": "cross_above"}, "channels": ["email"], "active": true, "name": "AAPL Alert"}' + +# Push a tick via Redis +redis-cli LPUSH notiflo:ticks '{"symbol":"AAPL","value":160,"timestampUs":1708300000000000}' ``` -nx run-many -t + +## Evaluation Strategies + +| Strategy | Use Case | Complexity | Latency | +|----------|----------|------------|---------| +| `threshold_crossing` | Price alerts, sensor thresholds | O(1) amortized (Drift Sentinel) | ~75ns | +| `expression` | Compound conditions (`value > 150 AND volume > 1M`) | O(1) per condition | ~100ns-1us | +| `script` | Complex logic (Rhai sandbox with full scripting) | Varies | ~1-10us | + +## Delivery Channels + +| Channel | Provider | +|---------|----------| +| `email` | SendGrid, SMTP | +| `sms` | Twilio | +| `push` | FCM, APNs | +| `webhook` | HTTP POST | +| `in_app` | Internal store | +| `slack` | Slack API | +| `whatsapp` | Twilio / WhatsApp Business | + +## Configuration + +### Rust Runtime (environment variables) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `NOTIFLO_MONGODB_URI` | Yes | -- | MongoDB connection string | +| `NOTIFLO_REDIS_URL` | Yes | -- | Redis connection string | +| `NOTIFLO_INGEST_TYPE` | No | `redis` | `redis` or `websocket` | +| `NOTIFLO_REDIS_QUEUE_KEY` | No | `notiflo:ticks` | Redis list key for tick ingestion | +| `NOTIFLO_WS_URL` | If websocket | -- | WebSocket endpoint URL | +| `NOTIFLO_CONFIG_POLL_INTERVAL_MS` | No | `5000` | Config reload interval (ms) | +| `NOTIFLO_HEALTH_PORT` | No | `8080` | Health check HTTP port | +| `RUST_LOG` | No | `notiflo_runtime=info` | Log level | + +### NestJS API (environment variables) + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `MONGODB_URI` | Yes | -- | MongoDB connection string | +| `REDIS_URL` | No | -- | Redis for stream consumption | +| `PORT` | No | `3000` | HTTP port | + +## Development + +### Prerequisites + +- Rust 1.78+ +- Node.js 20+ +- Yarn +- MongoDB 7+ +- Redis 7+ + +### Build + +```bash +# Rust runtime +cargo build --release --bin notiflo-runtime + +# NestJS API +yarn install +npx nx build notiflo ``` -..or add `-p` to filter specific projects +### Test + +```bash +# Rust unit tests +cargo test --workspace --no-default-features + +# Rust integration tests (requires running MongoDB + Redis) +cargo test --tests -p notiflo-runtime --no-default-features --features integration-tests +# NestJS unit tests +npx nx test notiflo + +# NestJS E2E tests (requires running Redis) +npx nx e2e notiflo-e2e ``` -nx run-many -t -p + +### Benchmarks + +```bash +# Pipeline benchmarks (throughput, template render, delivery) +cargo bench --bench pipeline_bench --no-default-features + +# Condition evaluation benchmarks (Drift Sentinel, expression, script) +cargo bench --bench condition_bench --no-default-features ``` -Targets can be defined in the `package.json` or `projects.json`. Learn more [in the docs](https://nx.dev/core-features/run-tasks). +### Load Testing -## Want better Editor Integration? +The runtime includes a built-in load test binary. It runs the full pipeline (ingest, evaluate, deliver) against real infrastructure. -Have a look at the [Nx Console extensions](https://nx.dev/nx-console). It provides autocomplete support, a UI for exploring and running tasks & generators, and more! Available for VSCode, IntelliJ and comes with a LSP for Vim users. +```bash +cargo run --release --bin load-test --no-default-features -- \ + --conditions 10000 \ + --ticks 50000 +``` + +### Run Locally -## Ready to deploy? +```bash +# Start infrastructure only +docker compose up -d mongodb redis -Just run `nx build demoapp` to build the application. The build artifacts will be stored in the `dist/` directory, ready to be deployed. +# Run the Rust runtime +NOTIFLO_MONGODB_URI=mongodb://localhost:27017/notiflo \ +NOTIFLO_REDIS_URL=redis://localhost:6379 \ + cargo run --release --bin notiflo-runtime + +# Run the NestJS API +yarn install +npx nx serve notiflo +``` -## Set up CI! +## CI -Nx comes with local caching already built-in (check your `nx.json`). On CI you might want to go a step further. +The CI pipeline runs 5 jobs on every push: -- [Set up remote caching](https://nx.dev/core-features/share-your-cache) -- [Set up task distribution across multiple machines](https://nx.dev/nx-cloud/features/distribute-task-execution) -- [Learn more how to setup CI](https://nx.dev/recipes/ci) +| Job | What it checks | +|-----|---------------| +| `rust` | `cargo test`, `cargo clippy`, `cargo fmt` | +| `rust-integration` | Integration tests against real MongoDB + Redis | +| `node` | NestJS unit tests | +| `nestjs-e2e` | End-to-end tests against real services | +| `load-test-smoke` | Smoke run of the load test binary | -## Connect with us! +## License -- [Join the community](https://nx.dev/community) -- [Subscribe to the Nx Youtube Channel](https://www.youtube.com/@nxdevtools) -- [Follow us on Twitter](https://twitter.com/nxdevtools) +Apache 2.0 -- see [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ea7b407 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,44 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in Notiflo, please report it responsibly. + +**Do NOT open a public GitHub issue for security vulnerabilities.** + +Instead, email **security@notiflo.dev** with: + +1. Description of the vulnerability +2. Steps to reproduce +3. Affected versions +4. Any potential impact assessment + +We will acknowledge receipt within 48 hours and aim to provide a fix or mitigation plan within 7 days. + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| 0.1.x | Yes | + +## Security Considerations + +### Rust Runtime + +- The Rhai script sandbox has configurable execution timeouts to prevent resource exhaustion +- HTTP delivery uses TLS by default +- Redis connections support TLS via `rediss://` URLs +- No user-supplied input is passed to shell commands + +### NestJS API + +- API key authentication for organization-scoped endpoints +- Input validation via class-validator on all DTOs +- MongoDB injection protection through Mongoose schema validation +- CORS is configurable via `CORS_ORIGIN` environment variable + +### Infrastructure + +- MongoDB and Redis should be deployed with authentication enabled in production +- Use network policies to restrict access between services +- Rotate API keys regularly through the management API diff --git a/apps/notiflo-e2e/jest.config.ts b/apps/notiflo-e2e/jest.config.ts index 63cd03f..bdb197c 100644 --- a/apps/notiflo-e2e/jest.config.ts +++ b/apps/notiflo-e2e/jest.config.ts @@ -6,6 +6,9 @@ export default { globalTeardown: '/src/support/global-teardown.ts', setupFiles: ['/src/support/test-setup.ts'], testEnvironment: 'node', + // Run tests serially — E2E tests share a Redis stream consumer group, + // so parallel workers would steal each other's events. + maxWorkers: 1, transform: { '^.+\\.[tj]s$': [ 'ts-jest', @@ -16,4 +19,9 @@ export default { }, moduleFileExtensions: ['ts', 'js', 'html'], coverageDirectory: '../../coverage/notiflo-e2e', + testMatch: [ + '/src/**/*.spec.ts', + '/src/**/*.e2e-spec.ts', + ], + testTimeout: 30000, }; diff --git a/apps/notiflo-e2e/src/e2e/alert-lifecycle.spec.ts b/apps/notiflo-e2e/src/e2e/alert-lifecycle.spec.ts new file mode 100644 index 0000000..3966056 --- /dev/null +++ b/apps/notiflo-e2e/src/e2e/alert-lifecycle.spec.ts @@ -0,0 +1,161 @@ +import request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import Redis from 'ioredis'; +import { Db, MongoClient } from 'mongodb'; +import { + getApp, + createOrg, + createSubscriber, + createTemplate, + createAlert, +} from '../support/fixtures'; +import { getTestDb, assertDocExists } from '../support/db'; +import { getTestRedis, checkRedisAvailable } from '../support/redis'; +import { closeApp } from '../support/app-factory'; + +const STREAM_KEY = 'notiflo:events:delivery'; + +describe('Alert Lifecycle E2E (Real DB)', () => { + let app: INestApplication; + let mongoClient: MongoClient; + let db: Db; + + beforeAll(async () => { + app = await getApp(); + ({ client: mongoClient, db } = await getTestDb()); + }, 30000); + + afterAll(async () => { + await mongoClient?.close(); + await closeApp(); + }); + + it('full lifecycle: org -> subscriber -> template -> alert -> tick -> match', async () => { + // 1. Create organization + const org = await createOrg(app, { name: 'Lifecycle Org' }); + expect(org._id).toBeDefined(); + + // 2. Create subscriber + const sub = await createSubscriber(app, org._id, { + email: 'lifecycle@test.com', + channelPreferences: { email: { enabled: true }, sms: { enabled: true } }, + }); + expect(sub._id).toBeDefined(); + + // 3. Create template + const template = await createTemplate(app, org._id); + expect(template._id).toBeDefined(); + + // 4. Create alert condition + const alert = await createAlert(app, org._id, sub._id, { + symbol: 'MSFT', + strategyParams: { threshold: 400, operator: 'cross_above' }, + templateId: template._id, + }); + expect(alert._id).toBeDefined(); + + // Verify condition loaded in engine + const countRes = await request(app.getHttpServer()) + .get('/alerts/count') + .expect(200); + expect(countRes.body.count).toBeGreaterThanOrEqual(1); + + // 5. Submit tick that triggers match + const tickRes = await request(app.getHttpServer()) + .post('/alerts/ticks') + .send({ symbol: 'MSFT', value: 450, timestampUs: Date.now() * 1000 }) + .expect(200); + + expect(tickRes.body.count).toBe(1); + expect(tickRes.body.matches).toHaveLength(1); + expect(tickRes.body.matches[0].symbol).toBe('MSFT'); + expect(tickRes.body.matches[0].matchedValue).toBe(450); + + // 6. Submit tick below threshold — no match + const noMatchRes = await request(app.getHttpServer()) + .post('/alerts/ticks') + .send({ symbol: 'MSFT', value: 350, timestampUs: Date.now() * 1000 }) + .expect(200); + expect(noMatchRes.body.count).toBe(0); + + // 7. Verify alert is stored correctly in MongoDB + await assertDocExists(db, 'alertconditions', { symbol: 'MSFT' }); + + // 8. Verify subscriber in MongoDB + await assertDocExists(db, 'subscribers', { email: 'lifecycle@test.com' }); + + // 9. Dashboard endpoints work + const engineRes = await request(app.getHttpServer()) + .get('/dashboard/engine') + .expect(200); + expect(engineRes.body.available).toBe(true); + + const overviewRes = await request(app.getHttpServer()) + .get(`/dashboard/overview?orgId=${org._id}`) + .expect(200); + expect(overviewRes.body).toHaveProperty('totalNotificationsSent'); + + // 10. Delete alert and verify + await request(app.getHttpServer()) + .delete(`/alerts/${alert._id}`) + .expect(200); + + const afterCount = await request(app.getHttpServer()) + .get('/alerts/count') + .expect(200); + expect(afterCount.body.count).toBeLessThan(countRes.body.count); + }, 30000); + + it('delivery event flows from Redis stream to MongoDB notification (requires Redis)', async () => { + const redisAvailable = await checkRedisAvailable(); + if (!redisAvailable) { + console.log('Skipped: Redis not available'); + return; + } + + const org = await createOrg(app, { name: 'Stream Flow Org' }); + const redis = getTestRedis(); + const tsUs = String(Date.now() * 1000); + + try { + // Ensure consumer group exists (race with NestJS consumer init) + try { + await redis.xgroup('CREATE', STREAM_KEY, 'notiflo-api', '0', 'MKSTREAM'); + } catch (e: any) { + if (!e.message?.includes('BUSYGROUP')) throw e; + } + + // Simulate what the Rust runtime does after delivery + await redis.xadd( + STREAM_KEY, + '*', + 'request_id', 'req-e2e-flow', + 'condition_match_id', 'cond-e2e-flow', + 'organization_id', org._id, + 'subscriber_id', 'sub-e2e-flow', + 'channel', 'email', + 'provider', 'sendgrid', + 'success', 'true', + 'message_id', 'msg-e2e-flow', + 'error', '', + 'latency_us', '500', + 'timestamp_us', tsUs, + ); + + // Wait for consumer to process + await new Promise((r) => setTimeout(r, 3000)); + + // Verify notification appeared in MongoDB + const notification = await db.collection('notifications').findOne({ + organizationId: org._id, + }); + + expect(notification).not.toBeNull(); + expect(notification!.status).toBe('delivered'); + expect(notification!.channel).toBe('email'); + } finally { + await redis.xtrim(STREAM_KEY, 'MAXLEN', 0); + await redis.quit(); + } + }, 10000); +}); diff --git a/apps/notiflo-e2e/src/e2e/app.e2e.spec.ts b/apps/notiflo-e2e/src/e2e/app.e2e.spec.ts new file mode 100644 index 0000000..f4fbdeb --- /dev/null +++ b/apps/notiflo-e2e/src/e2e/app.e2e.spec.ts @@ -0,0 +1,127 @@ +import request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { + getApp, + createOrg, + createSubscriber, + createTemplate, + createAlert, +} from '../support/fixtures'; +import { closeApp } from '../support/app-factory'; + +describe('Notiflo E2E - Alert Lifecycle', () => { + let app: INestApplication; + + beforeAll(async () => { + app = await getApp(); + }, 30000); + + afterAll(async () => { + await closeApp(); + }); + + it('should complete the full alert lifecycle', async () => { + const org = await createOrg(app, { name: 'Test Org' }); + expect(org._id).toBeDefined(); + + const sub = await createSubscriber(app, org._id, { + email: 'e2e@test.com', + channelPreferences: { + email: { enabled: true }, + sms: { enabled: true }, + }, + }); + expect(sub._id).toBeDefined(); + + const template = await createTemplate(app, org._id); + expect(template._id).toBeDefined(); + + const alert = await createAlert(app, org._id, sub._id, { + symbol: 'AAPL', + strategyParams: { threshold: 150, operator: 'cross_above' }, + templateId: template._id, + }); + expect(alert._id).toBeDefined(); + expect(alert.symbol).toBe('AAPL'); + + // Verify Engine Has Condition Loaded + const countResponse = await request(app.getHttpServer()) + .get('/alerts/count') + .expect(200); + expect(countResponse.body.count).toBeGreaterThanOrEqual(1); + + // Submit Tick — Expect Match + const tickResponse = await request(app.getHttpServer()) + .post('/alerts/ticks') + .send({ symbol: 'AAPL', value: 160, timestampUs: Date.now() * 1000 }) + .expect(200); + expect(tickResponse.body.count).toBe(1); + expect(tickResponse.body.matches).toHaveLength(1); + expect(tickResponse.body.matches[0].symbol).toBe('AAPL'); + + // Submit Tick Below Threshold — No Match + const noMatchResponse = await request(app.getHttpServer()) + .post('/alerts/ticks') + .send({ symbol: 'AAPL', value: 140, timestampUs: Date.now() * 1000 }) + .expect(200); + expect(noMatchResponse.body.count).toBe(0); + + // Verify Engine Metrics + const metricsResponse = await request(app.getHttpServer()) + .get('/alerts/metrics') + .expect(200); + expect(metricsResponse.body.totalConditions).toBeGreaterThanOrEqual(1); + expect(metricsResponse.body.totalTicksProcessed).toBeGreaterThanOrEqual(2); + + // Dashboard Engine Endpoint + const dashboardEngineResponse = await request(app.getHttpServer()) + .get('/dashboard/engine') + .expect(200); + expect(dashboardEngineResponse.body.available).toBe(true); + + // Delete Alert + await request(app.getHttpServer()) + .delete(`/alerts/${alert._id}`) + .expect(200); + + const countAfterDelete = await request(app.getHttpServer()) + .get('/alerts/count') + .expect(200); + expect(countAfterDelete.body.count).toBeLessThan(countResponse.body.count); + }, 30000); + + it('should handle tick for unmatched symbol gracefully', async () => { + const response = await request(app.getHttpServer()) + .post('/alerts/ticks') + .send({ symbol: 'UNKNOWN_SYMBOL', value: 999, timestampUs: Date.now() * 1000 }) + .expect(200); + expect(response.body.count).toBe(0); + }); + + describe('Dashboard API E2E', () => { + it('should return engine status', async () => { + const res = await request(app.getHttpServer()) + .get('/dashboard/engine') + .expect(200); + expect(res.body).toHaveProperty('available'); + }); + + it('should return overview for an org', async () => { + const org = await createOrg(app, { name: 'Dashboard Org' }); + const res = await request(app.getHttpServer()) + .get(`/dashboard/overview?orgId=${org._id}`) + .expect(200); + expect(res.body).toHaveProperty('totalNotificationsSent'); + expect(res.body).toHaveProperty('deliveryRate'); + expect(res.body).toHaveProperty('channelBreakdown'); + }); + + it('should return channel health for an org', async () => { + const org = await createOrg(app, { name: 'Channel Health Org' }); + const res = await request(app.getHttpServer()) + .get(`/dashboard/channels?orgId=${org._id}`) + .expect(200); + expect(Array.isArray(res.body)).toBe(true); + }); + }); +}); diff --git a/apps/notiflo-e2e/src/integration/alerts-mongo.spec.ts b/apps/notiflo-e2e/src/integration/alerts-mongo.spec.ts new file mode 100644 index 0000000..2e8c120 --- /dev/null +++ b/apps/notiflo-e2e/src/integration/alerts-mongo.spec.ts @@ -0,0 +1,98 @@ +import request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { ObjectId } from 'mongodb'; +import { Db, MongoClient } from 'mongodb'; +import { getTestDb, cleanCollections, assertDocExists, countDocs } from '../support/db'; +import { getApp, createOrg, createSubscriber, createAlert } from '../support/fixtures'; +import { closeApp } from '../support/app-factory'; + +describe('Alerts <-> MongoDB Integration', () => { + let app: INestApplication; + let mongoClient: MongoClient; + let db: Db; + let orgId: string; + let subscriberId: string; + + beforeAll(async () => { + app = await getApp(); + ({ client: mongoClient, db } = await getTestDb()); + + const org = await createOrg(app); + orgId = org._id; + const sub = await createSubscriber(app, orgId); + subscriberId = sub._id; + }, 30000); + + afterAll(async () => { + await mongoClient?.close(); + await closeApp(); + }); + + beforeEach(async () => { + await cleanCollections(db, ['alertconditions']); + }); + + it('persists alert to MongoDB with correct schema', async () => { + const alert = await createAlert(app, orgId, subscriberId, { + symbol: 'TSLA', + strategyParams: { threshold: 200, operator: 'cross_above' }, + }); + + // Verify directly in MongoDB — bypass the API layer + const doc = await assertDocExists(db, 'alertconditions', { + _id: new ObjectId(alert._id), + }); + + expect(doc.symbol).toBe('TSLA'); + expect(doc.strategyType).toBe('threshold_crossing'); + expect(doc.strategyParams.threshold).toBe(200); + expect(doc.strategyParams.operator).toBe('cross_above'); + expect(doc.active).toBe(true); + expect(doc.organizationId).toBe(orgId); + expect(doc.subscriberId).toBe(subscriberId); + expect(doc.channels).toEqual(['email']); + }); + + it('updates alert in MongoDB', async () => { + const alert = await createAlert(app, orgId, subscriberId); + + await request(app.getHttpServer()) + .patch(`/alerts/${alert._id}`) + .send({ name: 'Updated Alert Name' }) + .expect(200); + + const doc = await assertDocExists(db, 'alertconditions', { + _id: new ObjectId(alert._id), + }); + expect(doc.name).toBe('Updated Alert Name'); + }); + + it('deletes alert from MongoDB', async () => { + const alert = await createAlert(app, orgId, subscriberId); + + const before = await countDocs(db, 'alertconditions'); + expect(before).toBe(1); + + await request(app.getHttpServer()) + .delete(`/alerts/${alert._id}`) + .expect(200); + + const after = await countDocs(db, 'alertconditions'); + expect(after).toBe(0); + }); + + it('persists multiple alerts and queries them', async () => { + await createAlert(app, orgId, subscriberId, { symbol: 'AAPL' }); + await createAlert(app, orgId, subscriberId, { symbol: 'GOOG' }); + await createAlert(app, orgId, subscriberId, { symbol: 'TSLA' }); + + const count = await countDocs(db, 'alertconditions'); + expect(count).toBe(3); + + // Verify via API too + const res = await request(app.getHttpServer()) + .get(`/alerts?organizationId=${orgId}`) + .expect(200); + expect(res.body).toHaveLength(3); + }); +}); diff --git a/apps/notiflo-e2e/src/integration/redis-stream.spec.ts b/apps/notiflo-e2e/src/integration/redis-stream.spec.ts new file mode 100644 index 0000000..61347ff --- /dev/null +++ b/apps/notiflo-e2e/src/integration/redis-stream.spec.ts @@ -0,0 +1,166 @@ +import { INestApplication } from '@nestjs/common'; +import Redis from 'ioredis'; +import { Db, MongoClient } from 'mongodb'; +import { getApp, createOrg } from '../support/fixtures'; +import { getTestDb, cleanCollections } from '../support/db'; +import { getTestRedis, checkRedisAvailable } from '../support/redis'; +import { closeApp } from '../support/app-factory'; + +const STREAM_KEY = 'notiflo:events:delivery'; + +describe('Redis Stream -> MongoDB Consumer Integration', () => { + let app: INestApplication; + let redis: Redis; + let mongoClient: MongoClient; + let db: Db; + let redisAvailable = false; + + beforeAll(async () => { + redisAvailable = await checkRedisAvailable(); + if (!redisAvailable) { + console.warn( + 'Redis not available — Redis stream tests will be skipped', + ); + return; + } + + app = await getApp(); + redis = getTestRedis(); + ({ client: mongoClient, db } = await getTestDb()); + + // Ensure the consumer group exists (the NestJS consumer creates it on init, + // but there's a race if it hasn't connected yet) + try { + await redis.xgroup('CREATE', STREAM_KEY, 'notiflo-api', '0', 'MKSTREAM'); + } catch (e: any) { + if (!e.message?.includes('BUSYGROUP')) throw e; + } + }, 30000); + + afterAll(async () => { + if (redis) await redis.quit(); + if (mongoClient) await mongoClient.close(); + await closeApp(); + }); + + afterEach(async () => { + if (!redisAvailable || !redis) return; + // Trim the stream instead of deleting it — preserves the consumer group + await redis.xtrim(STREAM_KEY, 'MAXLEN', 0); + if (db) await cleanCollections(db, ['notifications']); + }); + + it('consumer writes successful delivery to MongoDB', async () => { + if (!redisAvailable) { + console.log('Skipped: Redis not available'); + return; + } + + const orgId = `org-redis-test-${Date.now()}`; + const subId = `sub-redis-test-${Date.now()}`; + const tsUs = String(Date.now() * 1000); + + // Inject delivery event into Redis stream (mimics Rust runtime XADD) + await redis.xadd( + STREAM_KEY, + '*', + 'request_id', 'req-001', + 'condition_match_id', 'cond-001', + 'organization_id', orgId, + 'subscriber_id', subId, + 'channel', 'email', + 'provider', 'sendgrid', + 'success', 'true', + 'message_id', 'msg-001', + 'error', '', + 'latency_us', '1234', + 'timestamp_us', tsUs, + ); + + // Wait for NestJS consumer to pick it up (polls every 1s + 1s block) + await new Promise((r) => setTimeout(r, 3000)); + + const notification = await db.collection('notifications').findOne({ + organizationId: orgId, + subscriberId: subId, + }); + + expect(notification).not.toBeNull(); + expect(notification!.status).toBe('delivered'); + expect(notification!.channel).toBe('email'); + expect(notification!.provider).toBe('sendgrid'); + expect(notification!.result.success).toBe(true); + expect(notification!.result.messageId).toBe('msg-001'); + }, 10000); + + it('consumer marks failed delivery correctly', async () => { + if (!redisAvailable) { + console.log('Skipped: Redis not available'); + return; + } + + const orgId = `org-fail-${Date.now()}`; + + await redis.xadd( + STREAM_KEY, + '*', + 'request_id', 'req-fail-001', + 'condition_match_id', 'cond-fail-001', + 'organization_id', orgId, + 'subscriber_id', 'sub-fail-001', + 'channel', 'sms', + 'provider', 'twilio', + 'success', 'false', + 'message_id', '', + 'error', 'Connection timeout', + 'latency_us', '5000', + 'timestamp_us', String(Date.now() * 1000), + ); + + await new Promise((r) => setTimeout(r, 3000)); + + const notification = await db.collection('notifications').findOne({ + organizationId: orgId, + }); + + expect(notification).not.toBeNull(); + expect(notification!.status).toBe('failed'); + expect(notification!.result.success).toBe(false); + expect(notification!.result.error).toBe('Connection timeout'); + }, 10000); + + it('consumer processes a batch of events', async () => { + if (!redisAvailable) { + console.log('Skipped: Redis not available'); + return; + } + + const orgId = `org-batch-${Date.now()}`; + + // Insert 5 events rapidly + for (let i = 0; i < 5; i++) { + await redis.xadd( + STREAM_KEY, + '*', + 'request_id', `req-batch-${i}`, + 'condition_match_id', `cond-batch-${i}`, + 'organization_id', orgId, + 'subscriber_id', `sub-batch-${i}`, + 'channel', 'email', + 'provider', 'sendgrid', + 'success', 'true', + 'message_id', `msg-batch-${i}`, + 'error', '', + 'latency_us', '100', + 'timestamp_us', String(Date.now() * 1000), + ); + } + + await new Promise((r) => setTimeout(r, 3000)); + + const count = await db.collection('notifications').countDocuments({ + organizationId: orgId, + }); + expect(count).toBe(5); + }, 10000); +}); diff --git a/apps/notiflo-e2e/src/notiflo/notiflo.spec.ts b/apps/notiflo-e2e/src/notiflo/notiflo.spec.ts deleted file mode 100644 index e8ac2a6..0000000 --- a/apps/notiflo-e2e/src/notiflo/notiflo.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import axios from 'axios'; - -describe('GET /api', () => { - it('should return a message', async () => { - const res = await axios.get(`/api`); - - expect(res.status).toBe(200); - expect(res.data).toEqual({ message: 'Hello API' }); - }); -}); diff --git a/apps/notiflo-e2e/src/support/app-factory.ts b/apps/notiflo-e2e/src/support/app-factory.ts new file mode 100644 index 0000000..07734f6 --- /dev/null +++ b/apps/notiflo-e2e/src/support/app-factory.ts @@ -0,0 +1,76 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { + ENGINE_BRIDGE, + MockEngineBridgeService, + EngineBridgeService, +} from '@notiflo/bridge/napi-bridge'; +import { AppModule } from '../../../../apps/notiflo/src/app/app.module'; + +let app: INestApplication | null = null; +let mongod: MongoMemoryServer | null = null; +let mongoUri: string | null = null; + +/** + * Lazily creates a NestJS application backed by a real MongoDB instance. + * + * - If MONGODB_URI is already set (e.g. CI service container), uses it directly. + * - Otherwise starts MongoMemoryServer for local development. + */ +export async function getOrCreateApp(): Promise { + if (app) return app; + + mongod = await MongoMemoryServer.create(); + mongoUri = mongod.getUri(); + process.env.MONGODB_URI = mongoUri; + + // Ensure Redis URL is set (tests that need Redis will check availability separately) + const redisUrl = process.env.REDIS_URL ?? 'redis://localhost:6379'; + process.env.REDIS_URL = redisUrl; + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(EngineBridgeService) + .useClass(MockEngineBridgeService) + .overrideProvider(ENGINE_BRIDGE) + .useClass(MockEngineBridgeService) + .compile(); + + app = moduleFixture.createNestApplication(); + app.useGlobalPipes(new ValidationPipe({ transform: true })); + await app.init(); + + return app; +} + +/** + * Returns the MongoDB URI in use. + * Throws if the app has not been initialised yet. + */ +export function getMongoUri(): string { + if (!mongoUri) { + throw new Error( + 'App not initialized yet — call getOrCreateApp() first', + ); + } + return mongoUri; +} + +/** + * Gracefully shuts down the NestJS app and stops the in-memory MongoDB (if used). + * Safe to call multiple times. + */ +export async function closeApp(): Promise { + if (app) { + await app.close(); + app = null; + } + if (mongod) { + await mongod.stop(); + mongod = null; + delete process.env.MONGODB_URI; + } + mongoUri = null; +} diff --git a/apps/notiflo-e2e/src/support/db.ts b/apps/notiflo-e2e/src/support/db.ts new file mode 100644 index 0000000..53adf00 --- /dev/null +++ b/apps/notiflo-e2e/src/support/db.ts @@ -0,0 +1,56 @@ +import { MongoClient, Db } from 'mongodb'; +import { getMongoUri } from './app-factory'; + +/** + * Opens a raw MongoClient connection to the same in-memory MongoDB instance + * that the NestJS app is using. Callers MUST close the client in afterAll. + */ +export async function getTestDb(): Promise<{ client: MongoClient; db: Db }> { + const uri = getMongoUri(); + const client = new MongoClient(uri); + await client.connect(); + + // MongoMemoryServer URIs look like mongodb://127.0.0.1:PORT/ + // Mongoose typically uses the default db name from the URI or 'test'. + const dbName = new URL(uri).pathname.slice(1) || 'test'; + return { client, db: client.db(dbName) }; +} + +/** + * Deletes all documents from the specified collections. + */ +export async function cleanCollections( + db: Db, + collections: string[], +): Promise { + await Promise.all(collections.map((c) => db.collection(c).deleteMany({}))); +} + +/** + * Asserts that a document matching `filter` exists in `collection`. + * Returns the document if found; throws if not. + */ +export async function assertDocExists( + db: Db, + collection: string, + filter: Record, +): Promise { + const doc = await db.collection(collection).findOne(filter); + if (!doc) { + throw new Error( + `Expected document in '${collection}' matching ${JSON.stringify(filter)} — not found`, + ); + } + return doc; +} + +/** + * Returns the count of documents matching `filter` in `collection`. + */ +export async function countDocs( + db: Db, + collection: string, + filter: Record = {}, +): Promise { + return db.collection(collection).countDocuments(filter); +} diff --git a/apps/notiflo-e2e/src/support/fixtures.ts b/apps/notiflo-e2e/src/support/fixtures.ts new file mode 100644 index 0000000..32e8155 --- /dev/null +++ b/apps/notiflo-e2e/src/support/fixtures.ts @@ -0,0 +1,102 @@ +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { getOrCreateApp } from './app-factory'; + +/** + * Returns the shared NestJS application instance. + * Ensures the app is booted before returning. + */ +export async function getApp(): Promise { + return getOrCreateApp(); +} + +/** + * Creates an organization via the API and returns the response body. + */ +export async function createOrg( + app: INestApplication, + overrides: Record = {}, +): Promise { + const slug = `test-org-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const res = await request(app.getHttpServer()) + .post('/organizations') + .send({ name: 'Test Org', slug, ...overrides }) + .expect(201); + return res.body; +} + +/** + * Creates a subscriber via the API and returns the response body. + */ +export async function createSubscriber( + app: INestApplication, + orgId: string, + overrides: Record = {}, +): Promise { + const res = await request(app.getHttpServer()) + .post('/subscribers') + .send({ + organizationId: orgId, + externalId: `sub-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, + email: `test-${Date.now()}@example.com`, + channelPreferences: { email: { enabled: true } }, + ...overrides, + }) + .expect(201); + return res.body; +} + +/** + * Creates an alert condition via the API and returns the response body. + */ +export async function createAlert( + app: INestApplication, + orgId: string, + subscriberId: string, + overrides: Record = {}, +): Promise { + const res = await request(app.getHttpServer()) + .post('/alerts') + .send({ + organizationId: orgId, + subscriberId, + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { threshold: 150, operator: 'cross_above' }, + channels: ['email'], + active: true, + name: `Alert ${Date.now()}`, + ...overrides, + }) + .expect(201); + return res.body; +} + +/** + * Creates a notification template via the API and returns the response body. + */ +export async function createTemplate( + app: INestApplication, + orgId: string, + overrides: Record = {}, +): Promise { + const res = await request(app.getHttpServer()) + .post('/templates') + .send({ + organizationId: orgId, + name: `Template ${Date.now()}`, + channels: { + email: { + subject: 'Alert: {{symbol}}', + body: '{{symbol}} matched at {{matchedValue}}', + }, + }, + variables: [ + { name: 'symbol', type: 'string', required: true }, + { name: 'matchedValue', type: 'number', required: true }, + ], + ...overrides, + }) + .expect(201); + return res.body; +} diff --git a/apps/notiflo-e2e/src/support/global-setup.ts b/apps/notiflo-e2e/src/support/global-setup.ts index c1f5144..a6a79a6 100644 --- a/apps/notiflo-e2e/src/support/global-setup.ts +++ b/apps/notiflo-e2e/src/support/global-setup.ts @@ -1,10 +1,8 @@ /* eslint-disable */ -var __TEARDOWN_MESSAGE__: string; module.exports = async function () { - // Start services that that the app needs to run (e.g. database, docker-compose, etc.). - console.log('\nSetting up...\n'); - - // Hint: Use `globalThis` to pass variables to global teardown. - globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n'; + console.log('\nE2E Setup: global-setup invoked.'); + console.log( + 'The NestJS app + MongoMemoryServer are bootstrapped lazily via app-factory.ts in each test worker.\n', + ); }; diff --git a/apps/notiflo-e2e/src/support/global-teardown.ts b/apps/notiflo-e2e/src/support/global-teardown.ts index 32ea345..4b163aa 100644 --- a/apps/notiflo-e2e/src/support/global-teardown.ts +++ b/apps/notiflo-e2e/src/support/global-teardown.ts @@ -1,7 +1,6 @@ /* eslint-disable */ module.exports = async function () { - // Put clean up logic here (e.g. stopping services, docker-compose, etc.). - // Hint: `globalThis` is shared between setup and teardown. - console.log(globalThis.__TEARDOWN_MESSAGE__); + console.log('\nE2E Teardown: global-teardown invoked.'); + console.log('App and MongoMemoryServer are cleaned up via afterAll hooks in each test worker.\n'); }; diff --git a/apps/notiflo-e2e/src/support/redis.ts b/apps/notiflo-e2e/src/support/redis.ts new file mode 100644 index 0000000..e4d5080 --- /dev/null +++ b/apps/notiflo-e2e/src/support/redis.ts @@ -0,0 +1,73 @@ +import Redis from 'ioredis'; + +/** + * Creates a fresh ioredis client connected to the test Redis instance. + * Callers MUST call redis.quit() in afterAll/afterEach. + */ +export function getTestRedis(): Redis { + const url = process.env.REDIS_URL ?? 'redis://localhost:6379'; + return new Redis(url); +} + +/** + * Probes Redis availability by attempting a PING. + * Returns true if Redis is reachable, false otherwise. + */ +export async function checkRedisAvailable(): Promise { + const url = process.env.REDIS_URL ?? 'redis://localhost:6379'; + try { + const redis = new Redis(url, { + lazyConnect: true, + connectTimeout: 3000, + }); + await redis.connect(); + await redis.ping(); + await redis.quit(); + return true; + } catch { + return false; + } +} + +/** + * Polls a Redis stream until an entry matching `filter` appears, + * or throws after `timeoutMs`. + */ +export async function waitForStreamEntry( + redis: Redis, + streamKey: string, + filter: Record, + timeoutMs = 5000, +): Promise> { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const entries = await redis.xrange(streamKey, '-', '+'); + for (const [, fields] of entries) { + const map = parseStreamFields(fields); + const matches = Object.entries(filter).every( + ([k, v]) => map[k] === v, + ); + if (matches) return map; + } + await sleep(100); + } + throw new Error( + `No stream entry matching ${JSON.stringify(filter)} in '${streamKey}' within ${timeoutMs}ms`, + ); +} + +/** + * Converts the flat [key, val, key, val, ...] array returned by + * XRANGE/XREADGROUP into a key-value object. + */ +export function parseStreamFields(fields: string[]): Record { + const map: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + map[fields[i]] = fields[i + 1]; + } + return map; +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} diff --git a/apps/notiflo-e2e/src/support/test-setup.ts b/apps/notiflo-e2e/src/support/test-setup.ts index 07f2870..247c8e2 100644 --- a/apps/notiflo-e2e/src/support/test-setup.ts +++ b/apps/notiflo-e2e/src/support/test-setup.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -import axios from 'axios'; - +/** + * Jest setupFiles — runs in the same process as the tests, + * before each test suite is executed. + */ module.exports = async function () { - // Configure axios for tests to use. - const host = process.env.HOST ?? 'localhost'; - const port = process.env.PORT ?? '3000'; - axios.defaults.baseURL = `http://${host}:${port}`; + // Increase default timeout for E2E tests (app boot can take a while) + jest.setTimeout(30000); }; diff --git a/apps/notiflo-web-e2e/.eslintrc.json b/apps/notiflo-web-e2e/.eslintrc.json new file mode 100644 index 0000000..a6ed4fc --- /dev/null +++ b/apps/notiflo-web-e2e/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.cy.{ts,js,tsx,jsx}", "src/**/*.{ts,js,tsx,jsx}"], + "rules": {} + } + ] +} diff --git a/apps/notiflo-web-e2e/cypress.config.ts b/apps/notiflo-web-e2e/cypress.config.ts new file mode 100644 index 0000000..293ed2f --- /dev/null +++ b/apps/notiflo-web-e2e/cypress.config.ts @@ -0,0 +1,6 @@ +import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: nxE2EPreset(__filename, { cypressDir: 'src' }), +}); diff --git a/apps/notiflo-web-e2e/project.json b/apps/notiflo-web-e2e/project.json new file mode 100644 index 0000000..2283a51 --- /dev/null +++ b/apps/notiflo-web-e2e/project.json @@ -0,0 +1,29 @@ +{ + "name": "notiflo-web-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/notiflo-web-e2e/src", + "targets": { + "e2e": { + "executor": "@nx/cypress:cypress", + "options": { + "cypressConfig": "apps/notiflo-web-e2e/cypress.config.ts", + "testingType": "e2e", + "devServerTarget": "notiflo-web:serve" + }, + "configurations": { + "production": { + "devServerTarget": "notiflo-web:serve:production" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/notiflo-web-e2e/**/*.{js,ts}"] + } + } + }, + "tags": [], + "implicitDependencies": ["notiflo-web"] +} diff --git a/apps/notiflo-web-e2e/src/e2e/alert-flow.cy.ts b/apps/notiflo-web-e2e/src/e2e/alert-flow.cy.ts new file mode 100644 index 0000000..1e23c70 --- /dev/null +++ b/apps/notiflo-web-e2e/src/e2e/alert-flow.cy.ts @@ -0,0 +1,116 @@ +import { + getAlertSymbol, + getAlertSubscriber, + getAlertSubmit, + getTickSymbol, + getTickValue, + getTickSubmit, + getNavNotifications, +} from '../support/app.po'; + +describe('Alert Flow - Golden Path', () => { + beforeEach(() => cy.visit('/alerts')); + + it('should render the alerts page with form and table', () => { + cy.get('[data-testid="alerts-page"]').should('exist'); + cy.get('[data-testid="alert-form"]').should('exist'); + cy.get('[data-testid="tick-form"]').should('exist'); + }); + + it('should have submit button disabled when fields are empty', () => { + getAlertSubmit().should('be.disabled'); + }); + + it('should enable submit when required fields are filled', () => { + getAlertSymbol().type('AAPL'); + getAlertSubscriber().type('sub-test-1'); + getAlertSubmit().should('not.be.disabled'); + }); + + it('should create an alert and show success message', () => { + // Intercept the API call + cy.intercept('POST', '/api/alerts', { + statusCode: 201, + body: { + _id: 'alert-e2e-1', + organizationId: 'default-org', + subscriberId: 'sub-test-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { targetPrice: 150, direction: 'above' }, + channels: ['email'], + active: true, + createdAt: new Date().toISOString(), + }, + }).as('createAlert'); + + // Also intercept the refetch + cy.intercept('GET', '/api/alerts*', { + statusCode: 200, + body: [ + { + _id: 'alert-e2e-1', + organizationId: 'default-org', + subscriberId: 'sub-test-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { targetPrice: 150, direction: 'above' }, + channels: ['email'], + active: true, + name: 'AAPL Alert', + createdAt: new Date().toISOString(), + }, + ], + }).as('getAlerts'); + + getAlertSymbol().type('AAPL'); + getAlertSubscriber().type('sub-test-1'); + cy.get('[data-testid="alert-target-price"]').type('150'); + cy.get('[data-testid="alert-channel-email"]').check({ force: true }); + getAlertSubmit().click(); + + cy.wait('@createAlert'); + cy.get('[data-testid="alert-success"]').should('exist'); + }); + + it('should submit a tick and show results', () => { + cy.intercept('POST', '/api/alerts/ticks', { + statusCode: 200, + body: { matches: [{ alertId: 'alert-1' }], count: 1 }, + }).as('submitTick'); + + getTickSymbol().type('AAPL'); + getTickValue().type('155'); + getTickSubmit().click(); + + cy.wait('@submitTick'); + cy.get('[data-testid="tick-result"]').should('contain', '1 matches found'); + }); + + it('should navigate to notifications page', () => { + cy.intercept('GET', '/api/notifications*', { + statusCode: 200, + body: [ + { + _id: 'notif-1', + organizationId: 'default-org', + subscriberId: 'sub-test-1', + channel: 'email', + status: 'delivered', + provider: 'sendgrid', + content: {}, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + sentAt: new Date().toISOString(), + }, + ], + }).as('getNotifications'); + + getNavNotifications().click(); + cy.url().should('include', '/notifications'); + + cy.wait('@getNotifications'); + cy.get('[data-testid="notifications-table"]').should('exist'); + cy.get('[data-testid="status-badge-delivered"]').should('exist'); + }); +}); diff --git a/apps/notiflo-web-e2e/src/e2e/app.cy.ts b/apps/notiflo-web-e2e/src/e2e/app.cy.ts new file mode 100644 index 0000000..ca050f2 --- /dev/null +++ b/apps/notiflo-web-e2e/src/e2e/app.cy.ts @@ -0,0 +1,38 @@ +import { + getNavDashboard, + getNavAlerts, + getNavNotifications, +} from '../support/app.po'; + +describe('Notiflo Navigation', () => { + beforeEach(() => cy.visit('/')); + + it('should redirect to /dashboard from /', () => { + cy.url().should('include', '/dashboard'); + }); + + it('should render the sidebar with all nav links', () => { + getNavDashboard().should('exist'); + getNavAlerts().should('exist'); + getNavNotifications().should('exist'); + }); + + it('should navigate to alerts page', () => { + getNavAlerts().click(); + cy.url().should('include', '/alerts'); + cy.get('[data-testid="alerts-page"]').should('exist'); + }); + + it('should navigate to notifications page', () => { + getNavNotifications().click(); + cy.url().should('include', '/notifications'); + cy.get('[data-testid="notifications-page"]').should('exist'); + }); + + it('should navigate back to dashboard', () => { + getNavAlerts().click(); + getNavDashboard().click(); + cy.url().should('include', '/dashboard'); + cy.get('[data-testid="dashboard-page"]').should('exist'); + }); +}); diff --git a/apps/notiflo-web-e2e/src/e2e/dashboard.cy.ts b/apps/notiflo-web-e2e/src/e2e/dashboard.cy.ts new file mode 100644 index 0000000..d59fbac --- /dev/null +++ b/apps/notiflo-web-e2e/src/e2e/dashboard.cy.ts @@ -0,0 +1,24 @@ +describe('Dashboard Page', () => { + beforeEach(() => cy.visit('/dashboard')); + + it('should render the dashboard page heading', () => { + cy.get('[data-testid="dashboard-page"]').should('exist'); + cy.contains('h1', 'Dashboard').should('be.visible'); + }); + + it('should show overview metrics section (loading or data)', () => { + // Either loading state or the actual metrics should appear + cy.get('[data-testid="overview-metrics"], [data-testid="overview-loading"], [data-testid="overview-error"]') + .should('exist'); + }); + + it('should show channel health section', () => { + cy.get('[data-testid="channel-health-grid"], [data-testid="channel-health-loading"], [data-testid="channel-health-error"], [data-testid="channel-health-empty"]') + .should('exist'); + }); + + it('should show engine status section', () => { + cy.get('[data-testid="engine-status"], [data-testid="engine-status-loading"], [data-testid="engine-status-error"]') + .should('exist'); + }); +}); diff --git a/apps/notiflo-web-e2e/src/fixtures/example.json b/apps/notiflo-web-e2e/src/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/apps/notiflo-web-e2e/src/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/apps/notiflo-web-e2e/src/support/app.po.ts b/apps/notiflo-web-e2e/src/support/app.po.ts new file mode 100644 index 0000000..5c310d0 --- /dev/null +++ b/apps/notiflo-web-e2e/src/support/app.po.ts @@ -0,0 +1,28 @@ +// Page object helpers for Notiflo E2E tests + +// Navigation +export const getNavDashboard = () => cy.get('[data-testid="nav-dashboard"]'); +export const getNavAlerts = () => cy.get('[data-testid="nav-alerts"]'); +export const getNavNotifications = () => cy.get('[data-testid="nav-notifications"]'); + +// Dashboard +export const getOverviewMetrics = () => cy.get('[data-testid="overview-metrics"]'); +export const getChannelHealthGrid = () => cy.get('[data-testid="channel-health-grid"]'); +export const getEngineStatus = () => cy.get('[data-testid="engine-status"]'); + +// Alerts +export const getAlertForm = () => cy.get('[data-testid="alert-form"]'); +export const getAlertSymbol = () => cy.get('[data-testid="alert-symbol"]'); +export const getAlertSubscriber = () => cy.get('[data-testid="alert-subscriber"]'); +export const getAlertSubmit = () => cy.get('[data-testid="alert-submit"]'); +export const getAlertsTable = () => cy.get('[data-testid="alerts-table"]'); + +// Tick +export const getTickForm = () => cy.get('[data-testid="tick-form"]'); +export const getTickSymbol = () => cy.get('[data-testid="tick-symbol"]'); +export const getTickValue = () => cy.get('[data-testid="tick-value"]'); +export const getTickSubmit = () => cy.get('[data-testid="tick-submit"]'); +export const getTickResult = () => cy.get('[data-testid="tick-result"]'); + +// Notifications +export const getNotificationsTable = () => cy.get('[data-testid="notifications-table"]'); diff --git a/apps/notiflo-web-e2e/src/support/commands.ts b/apps/notiflo-web-e2e/src/support/commands.ts new file mode 100644 index 0000000..c421a3c --- /dev/null +++ b/apps/notiflo-web-e2e/src/support/commands.ts @@ -0,0 +1,35 @@ +/// + +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(email: string, password: string): void; + } +} + +// -- This is a parent command -- +Cypress.Commands.add('login', (email, password) => { + console.log('Custom command example: Login', email, password); +}); +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/apps/notiflo-web-e2e/src/support/e2e.ts b/apps/notiflo-web-e2e/src/support/e2e.ts new file mode 100644 index 0000000..1c1a9e7 --- /dev/null +++ b/apps/notiflo-web-e2e/src/support/e2e.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.ts using ES2015 syntax: +import './commands'; diff --git a/apps/notiflo-web-e2e/tsconfig.json b/apps/notiflo-web-e2e/tsconfig.json new file mode 100644 index 0000000..e1eeabd --- /dev/null +++ b/apps/notiflo-web-e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["cypress", "node"], + "sourceMap": false + }, + "include": [ + "**/*.ts", + "**/*.js", + "cypress.config.ts", + "**/*.cy.ts", + "**/*.cy.tsx", + "**/*.cy.js", + "**/*.cy.jsx", + "**/*.d.ts" + ] +} diff --git a/apps/notiflo-web/.eslintrc.json b/apps/notiflo-web/.eslintrc.json new file mode 100644 index 0000000..fb7e570 --- /dev/null +++ b/apps/notiflo-web/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "extends": [ + "plugin:@nx/react-typescript", + "next", + "next/core-web-vitals", + "../../.eslintrc.json" + ], + "ignorePatterns": ["!**/*", ".next/**/*"], + "overrides": [ + { + "files": ["*.*"], + "rules": { + "@next/next/no-html-link-for-pages": "off" + } + }, + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@next/next/no-html-link-for-pages": ["error", "apps/notiflo-web/pages"] + } + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], + "env": { + "jest": true + } + } + ] +} diff --git a/apps/notiflo-web/components/alerts/AlertsList.spec.tsx b/apps/notiflo-web/components/alerts/AlertsList.spec.tsx new file mode 100644 index 0000000..ad8b83d --- /dev/null +++ b/apps/notiflo-web/components/alerts/AlertsList.spec.tsx @@ -0,0 +1,75 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import AlertsList from './AlertsList'; +import { Alert } from '../../lib/types'; + +const mockAlerts: Alert[] = [ + { + _id: 'alert-1', + organizationId: 'org-1', + subscriberId: 'sub-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { threshold: 150 }, + channels: ['email', 'sms'], + active: true, + name: 'Apple Alert', + createdAt: '2026-01-15T10:00:00Z', + }, + { + _id: 'alert-2', + organizationId: 'org-1', + subscriberId: 'sub-2', + symbol: 'GOOG', + strategyType: 'moving_average_crossover', + strategyParams: {}, + channels: ['push'], + active: false, + name: 'Google Alert', + createdAt: '2026-01-16T12:00:00Z', + }, +]; + +describe('AlertsList', () => { + it('renders loading state', () => { + render(); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('renders error state', () => { + render(); + expect(screen.getByText('Failed to load')).toBeInTheDocument(); + }); + + it('renders empty state with "No alerts" message', () => { + render(); + expect(screen.getByText('No alerts')).toBeInTheDocument(); + }); + + it('renders table with correct columns', () => { + render(); + expect(screen.getByTestId('alerts-table')).toBeInTheDocument(); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Symbol')).toBeInTheDocument(); + expect(screen.getByText('Strategy')).toBeInTheDocument(); + expect(screen.getByText('Channels')).toBeInTheDocument(); + expect(screen.getByText('Status')).toBeInTheDocument(); + expect(screen.getByText('Created')).toBeInTheDocument(); + }); + + it('renders alert data correctly', () => { + render(); + expect(screen.getByTestId('alert-row-alert-1')).toBeInTheDocument(); + expect(screen.getByTestId('alert-row-alert-2')).toBeInTheDocument(); + expect(screen.getByText('Apple Alert')).toBeInTheDocument(); + expect(screen.getByText('AAPL')).toBeInTheDocument(); + expect(screen.getByText('GOOG')).toBeInTheDocument(); + }); + + it('shows active/inactive status badge', () => { + render(); + expect(screen.getByText('Active')).toBeInTheDocument(); + expect(screen.getByText('Inactive')).toBeInTheDocument(); + }); +}); diff --git a/apps/notiflo-web/components/alerts/AlertsList.tsx b/apps/notiflo-web/components/alerts/AlertsList.tsx new file mode 100644 index 0000000..198a752 --- /dev/null +++ b/apps/notiflo-web/components/alerts/AlertsList.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { Alert } from '../../lib/types'; + +interface AlertsListProps { + alerts: Alert[] | null; + loading: boolean; + error: string | null; +} + +const strategyBadge: Record = { + threshold_crossing: { label: 'Threshold', className: 'badge-green' }, + expression: { label: 'Expression', className: 'badge-cyan' }, + script: { label: 'Rhai Script', className: 'badge-violet' }, +}; + +export default function AlertsList({ alerts, loading, error }: AlertsListProps) { + if (loading) { + return
Loading...
; + } + + if (error) { + return
{error}
; + } + + if (!alerts || alerts.length === 0) { + return ( +
+

No alerts

+

Create your first alert to start evaluating conditions.

+
+ ); + } + + return ( +
+ + + + + + + + + + + + + {alerts.map((alert, index) => { + const badge = strategyBadge[alert.strategyType] || { + label: alert.strategyType, + className: 'badge bg-elevated text-text-secondary border border-border', + }; + return ( + + + + + + + + + ); + })} + +
NameSymbolStrategyChannelsStatusCreated
{alert.name || '-'}{alert.symbol} + {badge.label} + +
+ {alert.channels.map((ch) => ( + + {ch} + + ))} +
+
+ + {alert.active ? 'Active' : 'Inactive'} + + + {new Date(alert.createdAt).toLocaleDateString()} +
+
+ ); +} diff --git a/apps/notiflo-web/components/alerts/CreateAlertForm.spec.tsx b/apps/notiflo-web/components/alerts/CreateAlertForm.spec.tsx new file mode 100644 index 0000000..9bfee7f --- /dev/null +++ b/apps/notiflo-web/components/alerts/CreateAlertForm.spec.tsx @@ -0,0 +1,143 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CreateAlertForm from './CreateAlertForm'; +import { createAlert } from '../../lib/api-client'; + +jest.mock('../../lib/api-client', () => ({ + createAlert: jest.fn(), +})); + +const mockedCreateAlert = createAlert as jest.MockedFunction; + +describe('CreateAlertForm', () => { + const onCreated = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all form fields', () => { + render(); + expect(screen.getByTestId('alert-form')).toBeInTheDocument(); + expect(screen.getByTestId('alert-name')).toBeInTheDocument(); + expect(screen.getByTestId('alert-symbol')).toBeInTheDocument(); + expect(screen.getByTestId('alert-strategy')).toBeInTheDocument(); + expect(screen.getByTestId('alert-target-price')).toBeInTheDocument(); + expect(screen.getByTestId('alert-direction')).toBeInTheDocument(); + expect(screen.getByTestId('alert-channel-email')).toBeInTheDocument(); + expect(screen.getByTestId('alert-channel-sms')).toBeInTheDocument(); + expect(screen.getByTestId('alert-channel-push')).toBeInTheDocument(); + expect(screen.getByTestId('alert-channel-in_app')).toBeInTheDocument(); + expect(screen.getByTestId('alert-subscriber')).toBeInTheDocument(); + expect(screen.getByTestId('alert-submit')).toBeInTheDocument(); + }); + + it('submit button is disabled when required fields empty', () => { + render(); + expect(screen.getByTestId('alert-submit')).toBeDisabled(); + }); + + it('enables submit when required fields filled', async () => { + render(); + await userEvent.type(screen.getByTestId('alert-symbol'), 'AAPL'); + await userEvent.type(screen.getByTestId('alert-subscriber'), 'sub-1'); + expect(screen.getByTestId('alert-submit')).toBeEnabled(); + }); + + it('calls createAlert with correct payload on submit', async () => { + mockedCreateAlert.mockResolvedValueOnce({ + _id: '1', + organizationId: 'default-org', + subscriberId: 'sub-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { threshold: 150, operator: 'cross_above' }, + channels: ['email'], + active: true, + createdAt: new Date().toISOString(), + }); + + render(); + await userEvent.type(screen.getByTestId('alert-name'), 'My Alert'); + await userEvent.type(screen.getByTestId('alert-symbol'), 'AAPL'); + await userEvent.type(screen.getByTestId('alert-target-price'), '150'); + await userEvent.click(screen.getByTestId('alert-channel-email')); + await userEvent.type(screen.getByTestId('alert-subscriber'), 'sub-1'); + await userEvent.click(screen.getByTestId('alert-submit')); + + await waitFor(() => { + expect(mockedCreateAlert).toHaveBeenCalledWith({ + organizationId: 'default-org', + subscriberId: 'sub-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: { threshold: 150, operator: 'cross_above' }, + channels: ['email'], + name: 'My Alert', + }); + }); + }); + + it('displays success message on successful creation', async () => { + mockedCreateAlert.mockResolvedValueOnce({ + _id: '1', + organizationId: 'default-org', + subscriberId: 'sub-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: {}, + channels: ['email'], + active: true, + createdAt: new Date().toISOString(), + }); + + render(); + await userEvent.type(screen.getByTestId('alert-symbol'), 'AAPL'); + await userEvent.type(screen.getByTestId('alert-subscriber'), 'sub-1'); + await userEvent.click(screen.getByTestId('alert-channel-email')); + await userEvent.click(screen.getByTestId('alert-submit')); + + await waitFor(() => { + expect(screen.getByTestId('alert-success')).toHaveTextContent('Alert created successfully'); + }); + }); + + it('displays error message on API failure', async () => { + mockedCreateAlert.mockRejectedValueOnce(new Error('Network error')); + + render(); + await userEvent.type(screen.getByTestId('alert-symbol'), 'AAPL'); + await userEvent.type(screen.getByTestId('alert-subscriber'), 'sub-1'); + await userEvent.click(screen.getByTestId('alert-channel-email')); + await userEvent.click(screen.getByTestId('alert-submit')); + + await waitFor(() => { + expect(screen.getByTestId('alert-error')).toHaveTextContent('Network error'); + }); + }); + + it('calls onCreated callback after successful creation', async () => { + mockedCreateAlert.mockResolvedValueOnce({ + _id: '1', + organizationId: 'default-org', + subscriberId: 'sub-1', + symbol: 'AAPL', + strategyType: 'threshold_crossing', + strategyParams: {}, + channels: [], + active: true, + createdAt: new Date().toISOString(), + }); + + render(); + await userEvent.type(screen.getByTestId('alert-symbol'), 'AAPL'); + await userEvent.type(screen.getByTestId('alert-subscriber'), 'sub-1'); + await userEvent.click(screen.getByTestId('alert-submit')); + + await waitFor(() => { + expect(onCreated).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/notiflo-web/components/alerts/CreateAlertForm.tsx b/apps/notiflo-web/components/alerts/CreateAlertForm.tsx new file mode 100644 index 0000000..cf27096 --- /dev/null +++ b/apps/notiflo-web/components/alerts/CreateAlertForm.tsx @@ -0,0 +1,424 @@ +import React, { useState, FormEvent } from 'react'; +import { createAlert } from '../../lib/api-client'; +import { CreateAlertPayload } from '../../lib/types'; + +interface CreateAlertFormProps { + onCreated: () => void; +} + +const STRATEGIES = [ + { + value: 'threshold_crossing', + label: 'Threshold', + description: 'B-tree sentinel check. Triggers when price crosses a boundary.', + latency: '~18ns', + color: 'neon-green', + icon: ( + + + + ), + }, + { + value: 'expression', + label: 'Expression', + description: 'DSL for compound conditions. Supports AND/OR/NOT operators.', + latency: '~50ns', + color: 'neon-cyan', + icon: ( + + + + ), + }, + { + value: 'script', + label: 'Rhai Script', + description: 'Sandboxed scripting engine. Full custom logic with safety limits.', + latency: '~1-5us', + color: 'neon-violet', + icon: ( + + + + ), + }, +] as const; + +const CHANNEL_OPTIONS = [ + { value: 'email', label: 'Email', icon: '@' }, + { value: 'sms', label: 'SMS', icon: '#' }, + { value: 'push', label: 'Push', icon: '!' }, + { value: 'in_app', label: 'In-App', icon: '*' }, +] as const; + +const RHAI_EXAMPLES = [ + { label: 'Simple threshold', code: 'value > 150.0' }, + { label: 'Volume-weighted', code: 'value > 150.0 && volume > 1_000_000.0' }, + { + label: 'Percentage change', + code: `let change_pct = (value - prev_close) / prev_close * 100.0; +change_pct > 5.0 || change_pct < -5.0`, + }, + { + label: 'Bollinger range', + code: `let mid = 150.0; +let band = 10.0; +value >= mid - band && value <= mid + band`, + }, +]; + +const EXPRESSION_EXAMPLES = [ + 'value > 150', + 'value >= 100 AND value <= 200', + 'price > 150 AND volume > 1000000', + '(value > 200 OR value < 50)', +]; + +export default function CreateAlertForm({ onCreated }: CreateAlertFormProps) { + const [name, setName] = useState(''); + const [symbol, setSymbol] = useState(''); + const [strategyType, setStrategyType] = useState('threshold_crossing'); + const [targetPrice, setTargetPrice] = useState(''); + const [direction, setDirection] = useState('above'); + const [expression, setExpression] = useState(''); + const [script, setScript] = useState(''); + const [channels, setChannels] = useState([]); + const [subscriberId, setSubscriberId] = useState(''); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const isValid = symbol.trim() !== '' && subscriberId.trim() !== ''; + + const handleChannelChange = (channel: string) => { + setChannels((prev) => + prev.includes(channel) + ? prev.filter((c) => c !== channel) + : [...prev, channel] + ); + }; + + const buildStrategyParams = (): Record => { + switch (strategyType) { + case 'threshold_crossing': + return { + threshold: targetPrice ? Number(targetPrice) : undefined, + operator: direction === 'above' ? 'cross_above' : 'cross_below', + }; + case 'expression': + return { expression: expression.trim() }; + case 'script': + return { script: script.trim() }; + default: + return {}; + } + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setSuccess(null); + setSubmitting(true); + + const payload: CreateAlertPayload = { + organizationId: 'default-org', + subscriberId: subscriberId.trim(), + symbol: symbol.trim().toUpperCase(), + strategyType, + strategyParams: buildStrategyParams(), + channels, + }; + + if (name.trim()) { + payload.name = name.trim(); + } + + try { + await createAlert(payload); + setSuccess('Alert created successfully'); + onCreated(); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'An error occurred'; + setError(message); + } finally { + setSubmitting(false); + } + }; + + const selectedStrategy = STRATEGIES.find((s) => s.value === strategyType); + + return ( +
+ {/* Strategy Selector Cards */} +
+ +
+ {STRATEGIES.map((strategy) => { + const isSelected = strategyType === strategy.value; + const colorMap: Record = { + 'neon-green': isSelected + ? 'border-neon-green/40 bg-glow-green shadow-glow-green' + : 'border-border hover:border-neon-green/20', + 'neon-cyan': isSelected + ? 'border-neon-cyan/40 bg-glow-cyan shadow-glow-cyan' + : 'border-border hover:border-neon-cyan/20', + 'neon-violet': isSelected + ? 'border-neon-violet/40 bg-glow-violet shadow-glow-violet' + : 'border-border hover:border-neon-violet/20', + }; + const textColor: Record = { + 'neon-green': isSelected ? 'text-neon-green' : 'text-text-secondary', + 'neon-cyan': isSelected ? 'text-neon-cyan' : 'text-text-secondary', + 'neon-violet': isSelected ? 'text-neon-violet' : 'text-text-secondary', + }; + + return ( + + ); + })} +
+
+ + {/* Common fields */} +
+
+ + setName(e.target.value)} + className="input-field" + placeholder="My Alert" + /> +
+
+ + setSymbol(e.target.value)} + className="input-field font-mono" + placeholder="AAPL" + /> +
+
+ + {/* Strategy-specific params */} +
+
+ + {selectedStrategy?.icon} + + + {selectedStrategy?.label} Parameters + +
+ + {strategyType === 'threshold_crossing' && ( +
+
+ + setTargetPrice(e.target.value)} + className="input-field font-mono" + placeholder="150.00" + /> +
+
+ + +
+
+ )} + + {strategyType === 'expression' && ( +
+ + setExpression(e.target.value)} + className="input-field font-mono text-neon-cyan" + placeholder="value > 150 AND volume > 1000000" + /> +
+ {EXPRESSION_EXAMPLES.map((ex) => ( + + ))} +
+

+ Fields: value, secondary_value (aliases: price, volume). Operators: {'>'} {'>='} {'<'} {'<='} == != AND OR NOT +

+
+ )} + + {strategyType === 'script' && ( +
+ +