diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..25dcdc4e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# AGENTS.md + +Conventions for AI coding agents working on this repo (Claude Code, +Codex, aider, etc.). The detailed engineering rules live in +[`CLAUDE.md`](./CLAUDE.md); this file is the short list of process +conventions that go *around* the code. Read both. + +## Always update `CHANGELOG.md` + +Every user-visible change in this repo lands with a `CHANGELOG.md` +entry under `## [Unreleased]`, in the same commit as the code change. +"User-visible" means anything an operator could notice: behaviour +change, new flag, new endpoint, removed flag, UI change, performance +characteristic, error-message format, dependency bump that changes +the operator surface. Internal refactors with no observable impact +don't need an entry, but when in doubt, write one. + +The file follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/): +- Sections: `### Added`, `### Changed`, `### Removed`, `### Fixed`, + `### Tests` (last is project-specific). +- Each bullet starts with a **bolded short headline**, then a colon, + then a self-contained explanation. Include *why* — not just *what*. +- Reference migration filenames, endpoint paths, env vars, and crate + names by their exact identifiers so the entry is greppable later. +- Test-only changes go under `### Tests`. + +If you're touching code without writing a CHANGELOG entry, you're +either doing the wrong thing or you forgot. Stop and add the entry +before staging the commit. + +## Use the generated OpenAPI SDK in `web/` + +The frontend has a generated TypeScript SDK at `web/src/api/client/` +(`types.gen.ts`, `sdk.gen.ts`, `@tanstack/react-query.gen.ts`) produced +by `bun run openapi-ts` against the running backend. **Use it.** + +- Do not write hand-rolled `fetch` helpers under `web/src/lib/`. There + used to be one (`backup-schedules.ts`) and it caused a real bug — + someone added a field to the backend, forgot to mirror it in the + shim's local type, and a UI feature silently dropped the field on + PATCH. +- If a binding you need is missing from the generated SDK, the cause + is the backend handler isn't fully decorated for OpenAPI. Fix it + there: add `#[utoipa::path]`, register the schema in `ApiDoc`, + restart the server, regenerate. Don't paper over with a `fetch` + shim. +- If you can't get the binding to generate, **ask for help** before + reaching for a shim. The shim creates two copies of the API surface + that drift apart. + +## Restart the server when you change the OpenAPI surface + +If your backend change touches handlers, request/response shapes, +schemas, or routes, you must: +1. Restart `temps serve` (use the `start-temps` skill). +2. `cd web && bun run openapi-ts` to regenerate the SDK against the + live server. +3. Commit the regenerated files. They're tracked in git on purpose so + reviewers see the API delta. + +The shortest way to spot a missing step: TypeScript compile errors +in `web/src/` that say "Module ... has no exported member ...". That +means the SDK is stale. + +## Pre-commit hooks run cargo fmt and cargo clippy + +Hooks **will** reformat your files and **will** fail the commit if +clippy finds issues. Plan for it: + +- Don't fight the formatter. If `cargo fmt` modifies a file during a + commit, re-stage and commit again. +- Multiple atomic commits run hooks once each. If you're committing + three related changes, prefer one commit so clippy/fmt run once. + (The wall-clock cost of clippy on this workspace is ~3–5 min.) +- Never pass `--no-verify` unless the user explicitly asks. CLAUDE.md + forbids it. If a hook is broken, fix the hook, don't bypass it. + +## Conventional Commits + +Already in CLAUDE.md, but reinforced here because it's a hard rule: +`type(scope): description` where type is one of `feat`, `fix`, +`docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, +`revert`. Scope is the affected crate or area (`backup`, `web`, +`deployments`, etc.). + +## Don't sweep unrelated dirty files into your commits + +If you arrive at a working tree that's already dirty (because a +previous session left files modified), confirm with the user whether +to include those files before staging them. Sweeping unrelated work +into a focused PR makes review slower and history harder to bisect. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3645f602..0fe5e656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- + +### Changed +- + +### Fixed +- + + +## [0.1.0-beta.19] - 2026-05-20 + +### Added +- **Manual (non-git) project creation from the CLI**: `bunx @temps-sdk/cli projects create` gains `--manual`, `--source-type` (`manual`, `docker_image`, or `static_files`), `--image`, and `--port` flags so you can create Docker-image and static-files projects without linking a git repository. The git-based flow is unchanged when `--repo` is supplied. + +### Fixed +- **AI Gateway returned 401 for valid API keys**: the OpenAI-compatible endpoints (`/ai/v1/chat/completions`, `/ai/v1/models`, `/ai/v1/embeddings`) were registered via `configure_public_routes`, which mounts on the no-auth router — but the handlers use the `RequireAuth` extractor, which reads the `AuthContext` injected by `auth_middleware`. Since that middleware only runs on the authenticated router, every request 401'd with "Authentication Required" *before* the `tk_` API key was ever validated, so no diagnostic was logged. The gateway routes now register via `configure_routes` alongside the admin/usage/pricing routes, so they sit on the authenticated surface: valid API keys authenticate and the `AiGatewayExecute` permission check runs as intended. +- **Static deployments were not served until an unrelated route reload**: `mark_deployment_complete` flipped `current_deployment_id` and fired the route-table `NOTIFY` before writing `static_dir_location`/`image_name`, which `load_routes()` reads to build an environment's backend. For static deployments the `NOTIFY` fired while `static_dir_location` was still NULL, so the proxy built a route with no static directory. A new Phase 0 step persists the routing-relevant deployment fields first, so the route table sees a consistent record the moment the `NOTIFY` fires. +- **Inflated session-engagement and bot traffic in analytics**: auto-fired view events (`page_view`, `page_leave`, `*_viewed`) — which intersection observers trigger for bots too — could mark a session "engaged" on their own. A session now counts as engaged only with ≥10s of measured wall-clock time or a genuine interaction event. Zero-duration session replays (never-finalized single-burst sessions, typically bots) are excluded from replay listings, and user-agent bot detection in the events pipeline is broadened. + + +## [0.1.0-beta.18] - 2026-05-19 + +### Added +- **Per-schedule backup scope — pick which databases a schedule backs up, and whether the control plane is included**: backup schedules used to fan out to every external service on the host unconditionally, with an unavoidable control-plane backup attached to every run. Two new boolean fields on `backup_schedules` give operators real control: `target_all_services` (defaults `true`) auto-includes every current and future external DB so the common case stays one-click, and a new `backup_schedule_services` join table (migration `m20260519_000001`) carries the explicit list when an operator opts into "Specific databases". `include_control_plane` (defaults `true`) lets schedules that exist purely to orchestrate external-DB backups drop the control-plane row. Service-layer validators (`BackupService::{create,update}_backup_schedule`) reject states that would have nothing to back up (control plane off + target_all_services off + no attached services); flipping `target_all_services → true` clears the explicit membership ("all means all"). Four new endpoints — `GET/POST /backups/schedules/{id}/services`, `DELETE /backups/schedules/{id}/services/{service_id}`, `GET /backups/external-services/{service_id}/schedules` — with audit logging and OpenAPI registration. UI: reusable `ScheduleServicesSelector` (checkbox list with indeterminate "Select all", hides already-attached); Create and Edit pages get an "All databases (recommended) / Specific databases" radio plus an "Also back up the Temps control plane" Switch; the schedule detail page surfaces both flags in the configuration card and only renders the per-service attach/detach card in 'specific' mode. Migration backfills existing rows to `target_all_services=true` and `include_control_plane=true` so behaviour is identical on upgrade. Covered by 6 unit tests (MockDatabase, Docker-skip) + 3 integration tests against TestDatabase (attach/detach round-trip, flip-to-all clears membership, fan-out skips control plane when flag is off). - **S3 bucket lifecycle rules enforce backup retention even when temps is offline**: every backup upload now carries `temps-managed=true` and `temps-retention-days=N` object tags (plus `temps-schedule-id` / `temps-backup-id` for traceability), and a new `S3LifecycleService` reconciles per-bucket `BucketLifecycleConfiguration` rules from current `backup_schedules` state. One tag-filtered rule per distinct retention value (`temps-retention-7d`, `temps-retention-30d`, …) so S3 expires expired objects autonomously. Reconcile fires fire-and-forget on schedule create/update/delete (only when `retention_period` or `enabled` changes), plus an hourly drift sweep that re-pushes the desired state — manual edits in the AWS console eventually converge. Tag-based filters were chosen over per-schedule prefixes so existing backup keys are untouched and restore still works; old objects (written before this change) simply lack the tags and are ignored by the rules. App-side `enforce_retention` still runs as the primary cleanup path; providers that reject `PutBucketLifecycleConfiguration` (Cloudflare R2, Backblaze B2, or insufficient IAM permissions) return `ReconcileOutcome::Unsupported` and we silently fall back — backups are never blocked because S3 rejected a lifecycle config. Live testcontainer roundtrip coverage against MinIO and RustFS validates the full `apply_lifecycle` → `get_bucket_lifecycle_configuration` shape; skips gracefully without Docker. Solves the "control plane offline for a week → storage costs balloon" failure mode. - **Public/admin console listener split**: the control plane can now bind admin/management routes (auth, dashboard, CRUD, settings, SwaggerUI, the SPA) to a separate address from public ingest (analytics events, error tracking, AI gateway, worker node sync, email tracking, Sentry/OTLP). Set `TEMPS_CONSOLE_ADMIN_ADDRESS=127.0.0.1:8081` (or any private interface) to enable; leave it unset for the existing single-listener behavior. Optional defense-in-depth via `TEMPS_ADMIN_ALLOWED_IPS` (comma-separated IPs/CIDRs), `TEMPS_ADMIN_ALLOWED_HOSTS` (comma-separated Host header values), and `TEMPS_ADMIN_TRUST_FORWARDED_FOR` (honor `X-Forwarded-For` only from loopback peers, anti-spoof). Denied requests on the admin gate return `404 Not Found`, not `403 Forbidden`, so probes can't fingerprint the admin surface. Each plugin classifies its own routes via the existing `configure_routes` (admin) / `configure_public_routes` (public) hooks — analytics events, session replay, performance, error tracking (Sentry + sentry-cli), email tracking, AI gateway, and the worker-facing multi-node endpoints have been split accordingly. SwaggerUI and the embedded SPA now mount on the admin listener only. See [docs/howto/admin-listener](docs/howto/admin-listener/page.mdx). - **Paginated "visitors in segment" page**: clicking any non-page dimension row (e.g. "Chrome" in Browsers, "United States" in Countries, an event name, a referrer, a UTM value) now navigates to `/projects/:slug/analytics/segments/:dimension/:value` — a paginated list of visitors that match the segment in the selected date range, sorted by last action descending (25 per page). Rows link to the existing visitor detail page so you can see the full journey for any visitor. Powered by new optional `filter_*` query params on `GET /analytics/visitors` (`filter_country`, `filter_region`, `filter_city`, `filter_channel`, `filter_referrer`, `filter_event`, `filter_browser`, `filter_os`, `filter_device`, `filter_language`, `filter_utm_source`, `filter_utm_medium`, `filter_utm_campaign`, `filter_utm_term`, `filter_utm_content`); visitor-side filters resolve against `visitor` / `ip_geolocations` while event-side filters use an `EXISTS (SELECT 1 FROM events …)` semi-join scoped by `(project_id, visitor_id, timestamp)` so existing composite indexes (`idx_events_visitor_timestamp`, `idx_visitor_project_last_seen`) carry the query. Date filter (quick or custom) is preserved across overview → dimensions → segment visitors → back. @@ -17,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Postgres WAL health probe + service-detail warning panel**: detects four "silent disk-filler" conditions on managed Postgres services (WAL bloat vs `max_wal_size`, stale replication slots, archive backlog, `archive_mode=on` with empty `archive_command`) and surfaces them on the service detail page with copy-to-clipboard remediation SQL. New `GET /external-services/{id}/wal-health` endpoint; snapshot persisted under the generic new `external_services.health_metadata` JSONB column so future engines can add sibling signals without further migrations. ### Changed +- **`EditBackupSchedule` page uses the generated OpenAPI SDK instead of a hand-rolled fetch shim**: `web/src/lib/backup-schedules.ts` (a hand-rolled `PATCH /api/backups/schedules/{id}` helper that predated the endpoint being in the OpenAPI surface) is deleted; the Edit page now uses `updateBackupScheduleMutation` and `UpdateBackupScheduleRequest` from the generated client. Removes a maintenance hazard where new fields on the request body had to be added in two places. Convention reinforced in `AGENTS.md`: hand-rolled `fetch` helpers under `web/src/lib/` are not allowed; if a binding is missing the fix is to expose the endpoint via `utoipa::path` and regenerate, not to write a shim. - **`temps login` is now browser-only for interactive use; `--api-key` is the headless path.** All credential entry happens in the web UI — there is no terminal password prompt anymore. Headless / CI authentication uses a pre-minted API key from the dashboard's **Settings → API Keys** page, passed via `--api-key`. - **Default agent turn caps raised**: committed agents now default to `max_turns: 30` (was 10), and the ephemeral dry-run cap rises to 50 (was 20). The Claude CLI invocation in `temps-agents` now treats `max_turns <= 0` as "omit the `--max-turns` flag entirely", letting a reviewed YAML opt into unlimited turns while `timeout_seconds` + `daily_budget_cents` still bound the run. @@ -25,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **CLI flags `--email` / `--password` / `--magic` / `--mfa` / `--device`** on `temps login`. The interactive flow is the browser device flow unconditionally; `--api-key` is preserved for headless / CI. Magic-link login through the CLI is no longer supported (magic links still work for browser logins from the web `/login` page). ### Fixed +- **Backup uploads to Cloudflare R2 no longer fail with `service error`**: every backup against an R2 bucket failed with `create_multipart_upload failed: service error` (5+ minute wall-clock, no diagnostic detail). Two root causes: (1) every S3 SDK call site rendered errors via `format!("...: {}", e)`, which for any 4xx/5xx collapses to the string "service error" — the HTTP status, service code, request id, and XML body were all thrown away; (2) the AWS SDK sends `x-amz-tagging` as a request header on `PutObject` and `CreateMultipartUpload`, and R2 returns `501 NotImplemented` on that header. Moving tagging to a follow-up `PutObjectTagging` call still failed — R2 also returns `501 NotImplemented` on that endpoint. Object tagging is simply not implemented on R2. Fix: added `describe_sdk_error` in `engines::v2_common` that pattern-matches every `SdkError` variant and surfaces HTTP status / service code / request_id / x-amz-id-2 / a truncated response body; all upload sites (single-part, create/upload/complete multipart, metadata companion, `head_bucket`) and the three `From for BackupError` impls now use it, so future S3 failures will say *what* actually went wrong. Tags are still applied via `PutObjectTagging` after every successful upload, but `apply_object_tags` now treats failures matching `is_unsupported_error` (NotImplemented, MethodNotAllowed, MalformedXML, AccessDenied, lifecycle-specific InvalidArgument) as best-effort — it logs a warn under target `temps_backup::tagging` and returns Ok so the backup itself succeeds. AWS S3 / MinIO / compliant stores still tag normally; tag-driven bucket lifecycle is unavailable on R2 (already best-effort in the reconciler) so app-side `BackupService::enforce_retention` is the retention source of truth there. Two regression tests pin the exact R2 error shapes for both the `x-amz-tagging` upload-header path and the `PutObjectTagging` path so a future SDK upgrade can't silently regress the matcher. - **GitHub App scoped token mint failures are now logged with context**: each fallible step of the GitHub App installation token flow (private key parse, JWT creation, octocrab client build, installation fetch, `access_tokens_url` parse, GitHub `access_tokens` POST) now emits an `error!` line with `installation_id` and `app_id` so a "GitHub rejected access_tokens" failure can be traced back to the specific installation. The new logs call out the two common causes — requested repo not selected on the installation, or the App lacks the requested permission — so operators stop having to re-derive context from the call site. Pure observability change; no behavior change to the token mint itself. - **Sandbox bring-up now runs a dedicated `normalize_ownership` step on both create and recover.** The container post-start chown is factored into a separate method that does `chown -R temps:temps` on both the home volume (best-effort: warns on non-zero exit, continues) and the bind-mounted `/home/temps/workspace` (fatal with `stat`-based verification so dev-machine bind-mount backends that return EPERM for logical no-ops don't abort, but real prod permission failures do). This is the in-container defense-in-depth that complements the host-side `chown_workdir_to_sandbox_user` from beta.9 — fixes the residual "Permission denied" failures on `mkdir reports/`, `git commit`, and lockfile creation under workspace. - **Postgres `archive_mode=on` with empty `archive_command` no longer causes runaway `pg_wal` growth.** Earlier versions baked `archive_mode=on` into the container CMD unconditionally, so any Postgres service whose `archive_command` was never set (no S3 source linked, or `enable_wal_archiving` never reached) accumulated WAL forever — we observed 191 GB `pg_wal` in production. New services now start with `archive_mode=off`; `enable_wal_archiving` recreates the container with `archive_mode=on` baked into CMD when WAL-G is configured. `PostgresService::start` reconciles by probing the volume for `walg.env` and comparing against the running container's CMD, recreating if they disagree — operator-initiated Stop/Start auto-repairs existing services with the bad combo. The bad combo is now unrepresentable for any service that's been restarted at least once. `start_service` also refreshes the WAL health snapshot inline after a recreate so the UI reflects the new state within ~1s instead of waiting for the next 30s probe cycle. diff --git a/Cargo.lock b/Cargo.lock index ad50b5b8..9b7a3720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-sink", @@ -30,11 +30,11 @@ dependencies = [ "actix-service", "actix-utils", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.1", "brotli 8.0.2", "bytes", "bytestring", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "flate2", "foldhash 0.1.5", @@ -65,14 +65,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "actix-router" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" dependencies = [ "bytestring", "cfg-if", @@ -104,7 +104,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 1.1.0", + "mio 1.2.0", "socket2 0.5.10", "tokio", "tracing", @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.11.0" +version = "4.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" dependencies = [ "actix-codec", "actix-http", @@ -149,7 +149,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie 0.16.2", - "derive_more 2.0.1", + "derive_more", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -167,7 +167,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.10", + "socket2 0.6.3", "time", "tracing", "url", @@ -182,7 +182,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -206,7 +206,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "generic-array", ] @@ -241,7 +241,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -274,6 +274,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -315,9 +324,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -330,44 +339,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -380,9 +389,12 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "arg_enum_proc_macro" @@ -392,7 +404,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -420,10 +432,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "ascii_utils" -version = "0.9.3" +name = "as-slice" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] [[package]] name = "askama" @@ -449,10 +464,10 @@ dependencies = [ "memchr", "proc-macro2", "quote", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_derive", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -464,7 +479,7 @@ dependencies = [ "memchr", "serde", "serde_derive", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] @@ -485,9 +500,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive 0.6.0", "asn1-rs-impl", @@ -495,7 +510,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -507,7 +522,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] @@ -519,7 +534,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] @@ -531,7 +546,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -546,48 +561,20 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693" +checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" dependencies = [ "filetime", "futures-core", "libc", "portable-atomic", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "tokio", "tokio-stream", "xattr", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "async-smtp" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d1f1a16e5abad3ada9f1f23dbc2f354b138121b90533381be62dada6cbf40a" -dependencies = [ - "anyhow", - "base64 0.13.1", - "futures", - "hostname 0.3.1", - "log", - "nom 7.1.3", - "pin-project", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -607,7 +594,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -618,7 +605,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -662,6 +649,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + [[package]] name = "av1-grain" version = "0.2.5" @@ -678,18 +685,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.8.14" +version = "1.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" dependencies = [ "aws-credential-types", "aws-runtime", @@ -701,13 +708,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-runtime", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "aws-types", "bytes", "fastrand", "hex", - "http 1.3.1", - "ring", + "http 1.4.0", + "sha1 0.10.6", "time", "tokio", "tracing", @@ -717,9 +725,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.12" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -729,9 +737,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -739,9 +747,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -751,9 +759,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.0" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -768,7 +776,7 @@ dependencies = [ "bytes-utils", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", "percent-encoding", @@ -779,9 +787,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.123.0" +version = "1.133.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c018f22146966fdd493a664f62ee2483dff256b42a08c125ab6a084bde7b77fe" +checksum = "237aba2985e3c0a83e199cc7aa9a64a16c599875bc98170f00932f6199f19922" dependencies = [ "aws-credential-types", "aws-runtime", @@ -800,23 +808,23 @@ dependencies = [ "bytes", "fastrand", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "lru", "percent-encoding", "regex-lite", - "sha2", + "sha2 0.11.0", "tracing", "url", ] [[package]] name = "aws-sdk-sesv2" -version = "1.113.0" +version = "1.119.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b804f9e1b36a40acbbd12052f664512828d3143a6ab809945a235aad018822" +checksum = "bf5a9e9bb406368fb7a1fc0134d0e7fa4b2ecdd1a7984bf528396ed1cede60b8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -831,16 +839,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "1.94.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699da1961a289b23842d88fe2984c6ff68735fdf9bdcbc69ceaeb2491c9bf434" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" dependencies = [ "aws-credential-types", "aws-runtime", @@ -855,16 +863,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.96.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e3a4cb3b124833eafea9afd1a6cc5f8ddf3efefffc6651ef76a03cbc6b4981" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" dependencies = [ "aws-credential-types", "aws-runtime", @@ -879,16 +887,16 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.98.0" +version = "1.104.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c4f19655ab0856375e169865c91264de965bd74c407c7f1e403184b1049409" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" dependencies = [ "aws-credential-types", "aws-runtime", @@ -904,16 +912,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -921,16 +929,15 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "crypto-bigint 0.5.5", + "crypto-bigint", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", - "http 1.3.1", - "p256 0.11.1", + "http 1.4.0", + "p256", "percent-encoding", - "ring", - "sha2", + "sha2 0.11.0", "subtle", "time", "tracing", @@ -939,9 +946,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.12" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -950,30 +957,30 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.4" +version = "0.64.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a764fa7222922f6c0af8eea478b0ef1ba5ce1222af97e01f33ca5e957bd7f3b9" +checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", "crc-fast", "hex", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", - "md-5", + "md-5 0.11.0", "pin-project-lite", - "sha1 0.10.6", - "sha2", + "sha1 0.11.0", + "sha2 0.11.0", "tracing", ] [[package]] name = "aws-smithy-eventstream" -version = "0.60.19" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ "aws-smithy-types", "bytes", @@ -982,9 +989,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.4" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -993,7 +1000,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "percent-encoding", @@ -1004,15 +1011,15 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "hyper", "hyper-rustls", "hyper-util", @@ -1022,33 +1029,35 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.62.4" +version = "0.62.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -1056,20 +1065,21 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" dependencies = [ "aws-smithy-async", "aws-smithy-http", "aws-smithy-http-client", "aws-smithy-observability", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "bytes", "fastrand", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1081,33 +1091,56 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.4" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "pin-project-lite", "tokio", "tracing", "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + [[package]] name = "aws-smithy-types" -version = "1.4.4" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" dependencies = [ "base64-simd", "bytes", "bytes-utils", "futures-core", "http 0.2.12", - "http 1.3.1", + "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -1124,22 +1157,23 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.14" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.12" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ "aws-credential-types", "aws-smithy-async", "aws-smithy-runtime-api", + "aws-smithy-schema", "aws-smithy-types", "rustc_version", "tracing", @@ -1155,7 +1189,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -1173,7 +1207,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -1181,16 +1215,16 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -1209,8 +1243,8 @@ dependencies = [ "sha1 0.10.6", "sync_wrapper", "tokio", - "tokio-tungstenite 0.28.0", - "tower 0.5.2", + "tokio-tungstenite 0.29.0", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -1225,7 +1259,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1239,13 +1273,13 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "mime", @@ -1258,13 +1292,13 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -1280,7 +1314,7 @@ dependencies = [ "bytes", "bytesize 1.3.3", "cookie 0.18.1", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "hyper-util", @@ -1293,23 +1327,23 @@ dependencies = [ "serde_urlencoded", "smallvec", "tokio", - "tower 0.5.2", + "tower 0.5.3", "url", ] [[package]] name = "axum-test" -version = "18.1.0" +version = "18.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680e88effaafbb28675074f29cda0e984c984bed5eb513085c17caf7de564225" +checksum = "0ce2a8627e8d8851f894696b39f2b67807d6375c177361d376173ace306a21e2" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "bytes", - "bytesize 2.1.0", + "bytesize 2.3.1", "cookie 0.18.1", "expect-json", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "hyper-util", @@ -1322,7 +1356,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "tokio", - "tower 0.5.2", + "tower 0.5.3", "url", ] @@ -1347,15 +1381,9 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - [[package]] name = "base16ct" version = "0.2.0" @@ -1404,9 +1432,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "basic-toml" @@ -1419,9 +1447,9 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" dependencies = [ "autocfg", "libm", @@ -1460,18 +1488,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] [[package]] name = "bitvec" @@ -1514,13 +1545,13 @@ dependencies = [ [[package]] name = "bollard" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227aa051deec8d16bd9c34605e7aaf153f240e35483dd42f6f78903847934738" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags 2.10.0", + "bitflags 2.11.1", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -1529,7 +1560,7 @@ dependencies = [ "futures-util", "hex", "home", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "hyper-named-pipe", @@ -1547,12 +1578,12 @@ dependencies = [ "serde_derive", "serde_json", "serde_urlencoded", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", "tokio-util", - "tonic 0.14.2", + "tonic 0.14.6", "tower-service", "url", "winapi 0.3.9", @@ -1564,9 +1595,9 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" dependencies = [ - "prost 0.14.1", - "prost-types 0.14.1", - "tonic 0.14.2", + "prost 0.14.3", + "prost-types 0.14.3", + "tonic 0.14.6", "tonic-prost", "ureq", ] @@ -1581,7 +1612,7 @@ dependencies = [ "bollard-buildkit-proto", "bytes", "chrono", - "prost 0.14.1", + "prost 0.14.3", "serde", "serde_json", "serde_repr", @@ -1590,25 +1621,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -1659,6 +1691,15 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bson" version = "2.15.0" @@ -1668,10 +1709,10 @@ dependencies = [ "ahash 0.8.12", "base64 0.22.1", "bitvec", - "getrandom 0.2.16", + "getrandom 0.2.17", "getrandom 0.3.4", "hex", - "indexmap 2.12.0", + "indexmap 2.14.0", "js-sys", "once_cell", "rand 0.9.4", @@ -1684,9 +1725,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -1695,15 +1736,15 @@ dependencies = [ [[package]] name = "built" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecheck" @@ -1735,9 +1776,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -1775,15 +1816,15 @@ checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" [[package]] name = "bytesize" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] @@ -1842,9 +1883,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", "serde_core", @@ -1861,7 +1902,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1871,11 +1912,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", - "cargo-platform 0.3.2", + "cargo-platform 0.3.3", "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1888,11 +1929,17 @@ dependencies = [ "toml 0.8.23", ] +[[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.43" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -1902,42 +1949,32 @@ dependencies = [ [[package]] name = "cf-rustracing" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f85c3824e4191621dec0551e3cef3d511f329da9a8990bf3e450a85651d97e" +checksum = "6565523d8145e63e0cf1b397a5f1bd4e90d5652a7dffb2de8cec460ff23ef6b1" dependencies = [ "backtrace", - "rand 0.8.6", + "rand 0.10.1", "tokio", "trackable", ] [[package]] name = "cf-rustracing-jaeger" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a5f80d44c257c3300a7f45ada676c211e64bbbac591bbec19344a8f61fbcab" +checksum = "16c0e4d8cce27f6a6eaff58d2b66f063a18b8ed0d6ef0947ae7a263afa3b7c08" dependencies = [ "cf-rustracing", - "hostname 0.4.1", + "hostname", "local-ip-address", "percent-encoding", - "rand 0.9.4", + "rand 0.10.1", "thrift_codec", "tokio", "trackable", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -1985,61 +2022,18 @@ dependencies = [ "zeroize", ] -[[package]] -name = "check-if-email-exists" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44233ea13b3021718dba6d2cc7d10e49ac37dba89a5c55a6cd914b931c3cbe6" -dependencies = [ - "anyhow", - "async-recursion", - "async-smtp", - "chrono", - "config", - "derive_builder 0.20.2", - "fantoccini", - "fast-socks5", - "futures", - "hickory-proto 0.24.4", - "hickory-resolver 0.24.4", - "levenshtein", - "log", - "mailchecker", - "md5", - "once_cell", - "rand 0.8.6", - "regex", - "reqwest", - "rustls", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", -] - [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", + "windows-link", ] [[package]] @@ -2048,7 +2042,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -2061,9 +2055,9 @@ checksum = "93a719913643003b84bd13022b4b7e703c09342cd03b679c4641c7d2e50dc34d" [[package]] name = "clap" -version = "4.5.50" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -2071,9 +2065,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.50" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -2083,21 +2077,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clickhouse" @@ -2135,7 +2129,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2145,13 +2139,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1678b3295890df5895480a7c080430e73df2b7101f1763f62a3b32dd532f1d37" dependencies = [ "chrono", - "http 1.3.1", + "http 1.4.0", "reqwest", "serde", "serde_json", "serde_urlencoded", - "serde_with 3.15.1", - "thiserror 2.0.17", + "serde_with 3.20.0", + "thiserror 2.0.18", "url", "urlencoding", "uuid", @@ -2163,13 +2157,13 @@ version = "0.14.1" source = "git+https://github.com/cloudflare/cloudflare-rs?rev=85fc25c#85fc25c18555ae8da1d75d5eba97199808f95d86" dependencies = [ "chrono", - "http 1.3.1", + "http 1.4.0", "reqwest", "serde", "serde_json", "serde_urlencoded", - "serde_with 3.15.1", - "thiserror 2.0.17", + "serde_with 3.20.0", + "thiserror 2.0.18", "url", "urlencoding", "uuid", @@ -2177,13 +2171,19 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.54" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "color_quant" version = "1.1.0" @@ -2192,9 +2192,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -2208,11 +2208,11 @@ dependencies = [ [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2297,7 +2297,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -2308,12 +2308,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.6.0" @@ -2332,6 +2326,15 @@ 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 = "cookie" version = "0.16.2" @@ -2400,28 +2403,26 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" dependencies = [ - "crc", "digest 0.10.7", - "rustversion", "spin 0.10.0", ] @@ -2511,18 +2512,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -2537,9 +2526,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -2548,9 +2537,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -2575,7 +2564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2608,6 +2597,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2632,7 +2630,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2683,6 +2681,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -2708,7 +2716,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2718,11 +2726,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2744,7 +2764,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2755,14 +2775,25 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -2774,9 +2805,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deadpool" @@ -2817,15 +2848,15 @@ dependencies = [ "blake2", "chacha20poly1305", "hex", - "hmac", + "hmac 0.12.1", "ip_network", "ip_network_table", "libc", - "nix 0.31.2", + "nix 0.31.3", "parking_lot", "ring", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.3", + "thiserror 2.0.18", "tracing", "uniffi", "untrusted", @@ -2834,9 +2865,9 @@ dependencies = [ [[package]] name = "defguard_wireguard_rs" -version = "0.9.4" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d031e0dd8796d520f18b21ce66c8769a78f91aa93c6bc8d51e6f2b6859a6a9c9" +checksum = "c25476f197cec498e72de55dcaa34f966720ae000eda3171f5144f0433256acc" dependencies = [ "base64 0.22.1", "defguard_boringtun", @@ -2845,14 +2876,14 @@ dependencies = [ "log", "netlink-packet-core 0.8.1", "netlink-packet-generic", - "netlink-packet-route 0.29.0", + "netlink-packet-route 0.30.0", "netlink-packet-utils 0.6.0", "netlink-packet-wireguard", "netlink-sys", - "nix 0.31.2", + "nix 0.31.3", "regex", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows 0.62.2", "wireguard-nt", "x25519-dalek", @@ -2860,19 +2891,9 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid 0.9.6", - "zeroize", -] +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" [[package]] name = "der" @@ -2907,7 +2928,7 @@ version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", "displaydoc", "nom 7.1.3", "num-bigint", @@ -2923,14 +2944,14 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -2955,18 +2976,18 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "derive-where" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -2977,7 +2998,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -3019,7 +3040,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -3039,40 +3060,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.108", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", - "syn 2.0.108", + "rustc_version", + "syn 2.0.117", "unicode-xid", ] @@ -3096,19 +3106,20 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -3161,7 +3172,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -3175,11 +3186,11 @@ dependencies = [ [[package]] name = "docker_credential" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "serde", "serde_json", ] @@ -3229,30 +3240,18 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] - [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.10", + "der", "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] @@ -3261,8 +3260,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", + "pkcs8", + "signature", ] [[package]] @@ -3274,7 +3273,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -3287,50 +3286,30 @@ checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest 0.10.7", - "ff 0.12.1", - "generic-array", - "group 0.12.1", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1 0.3.0", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", + "base16ct", + "crypto-bigint", "digest 0.10.7", - "ff 0.13.1", + "ff", "generic-array", - "group 0.13.0", + "group", "hkdf", "pem-rfc7468", - "pkcs8 0.10.2", + "pkcs8", "rand_core 0.6.4", - "sec1 0.7.3", + "sec1", "subtle", "zeroize", ] @@ -3369,12 +3348,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -3384,46 +3357,40 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "enumset" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", ] -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - [[package]] name = "env_logger" version = "0.10.2" @@ -3439,9 +3406,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -3467,7 +3434,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -3478,9 +3445,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -3540,37 +3507,38 @@ dependencies = [ [[package]] name = "expect-json" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7519e78573c950576b89eb4f4fe82aedf3a80639245afa07e3ee3d199dcdb29e" +checksum = "869f97f4abe8e78fc812a94ad6b721d72c4fb5532877c79610f2c238d7ccf6c4" dependencies = [ "chrono", "email_address", "expect-json-macros", "num", + "regex", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "typetag", "uuid", ] [[package]] name = "expect-json-macros" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bf7f5979e98460a0eb412665514594f68f366a32b85fa8d7ffb65bb1edee6a0" +checksum = "6e6fdf550180a6c29a28cb9aac262dc0064c25735641d2317f670075e9a469d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -3597,77 +3565,17 @@ dependencies = [ "regex", ] -[[package]] -name = "fantoccini" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a6a7a9a454c24453f9807c7f12b37e31ae43f3eb41888ae1f79a9a3e3be3f5" -dependencies = [ - "base64 0.22.1", - "cookie 0.18.1", - "futures-util", - "http 1.3.1", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "mime", - "serde", - "serde_json", - "time", - "tokio", - "url", - "webdriver", -] - -[[package]] -name = "fast-socks5" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89f36d4ee12370d30d57b16c7e190950a1a916e7dbbb5fd5a412f5ef913fe84" -dependencies = [ - "anyhow", - "async-trait", - "log", - "thiserror 1.0.69", - "tokio", - "tokio-stream", -] - -[[package]] -name = "fast_chemail" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" -dependencies = [ - "ascii_utils", -] - [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -3680,25 +3588,15 @@ dependencies = [ [[package]] name = "ferroid" -version = "0.8.9" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.9.4", + "rand 0.10.1", "web-time", ] -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.13.1" @@ -3717,21 +3615,19 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" @@ -3747,14 +3643,14 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-ng-sys", - "libz-rs-sys", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -3812,9 +3708,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "fs-err" @@ -3864,9 +3763,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -3879,9 +3778,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -3889,15 +3788,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -3917,38 +3816,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -3958,7 +3857,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -3970,9 +3868,9 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -4022,14 +3920,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -4061,6 +3959,18 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "ghash" version = "0.5.1" @@ -4073,9 +3983,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.3" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -4093,11 +4003,11 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -4132,24 +4042,13 @@ dependencies = [ "scroll", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.1", + "ff", "rand_core 0.6.4", "subtle", ] @@ -4166,7 +4065,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -4175,17 +4074,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", - "indexmap 2.12.0", + "http 1.4.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -4235,9 +4134,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -4264,9 +4174,9 @@ dependencies = [ [[package]] name = "headless_chrome" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "082c435ae636f62394526fe9c4c6b796efc1cec950c664456a44e5233ac5e2b8" +checksum = "333344ecb4b6a91ddd2e6a3c4fdb54aaddfbd2c82847f9c58fe42dd88afcf08e" dependencies = [ "anyhow", "auto_generate_cdp", @@ -4279,13 +4189,13 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tungstenite 0.28.0", "ureq", "url", "walkdir", "which", - "winreg 0.55.0", + "winreg", "zip 6.0.0", ] @@ -4314,29 +4224,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hickory-client" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ + "async-trait", "cfg-if", "data-encoding", "futures-channel", + "futures-io", "futures-util", - "hickory-proto 0.25.2", - "once_cell", - "radix_trie", - "rand 0.9.4", - "thiserror 2.0.17", + "hickory-proto 0.26.1", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "thiserror 2.0.18", + "tinyvec", "tokio", "tracing", + "url", ] [[package]] name = "hickory-proto" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "cfg-if", @@ -4348,8 +4263,9 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.8.6", - "thiserror 1.0.69", + "rand 0.9.4", + "ring", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -4358,89 +4274,89 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" dependencies = [ - "async-trait", - "cfg-if", "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", "idna", "ipnet", + "jni", "once_cell", - "rand 0.9.4", + "prefix-trie", + "rand 0.10.1", "ring", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", - "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" -version = "0.24.4" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-proto 0.24.4", + "hickory-proto 0.25.2", "ipconfig", - "lru-cache", + "moka", "once_cell", "parking_lot", - "rand 0.8.6", + "rand 0.9.4", "resolv-conf", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "hickory-resolver" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", - "hickory-proto 0.25.2", + "hickory-net", + "hickory-proto 0.26.1", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand 0.9.4", + "rand 0.10.1", "resolv-conf", "smallvec", - "thiserror 2.0.17", + "system-configuration", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "hickory-server" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585" +checksum = "130236ba6abba90da6a7acf7a87b27d862b592c3145dc74bc47bf86d8ff198ec" dependencies = [ "async-trait", "bytes", "cfg-if", "data-encoding", - "enum-as-inner", "futures-util", - "hickory-proto 0.25.2", + "hickory-net", + "hickory-proto 0.26.1", "ipnet", "prefix-trie", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-util", @@ -4453,7 +4369,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -4466,66 +4382,63 @@ dependencies = [ ] [[package]] -name = "home" -version = "0.5.12" +name = "hmac" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "windows-sys 0.61.2", + "digest 0.11.3", ] [[package]] -name = "hostname" -version = "0.3.1" +name = "home" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "libc", - "match_cfg", - "winapi 0.3.9", + "windows-sys 0.61.2", ] [[package]] name = "hostname" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" dependencies = [ "cfg-if", "libc", - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "htmd" -version = "0.5.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60ae59466542f2346e43d4a5e9b4432a1fc915b279c9fc0484e9ed7379121454" +checksum = "7eee9b00ee2e599b4f86507157e3db786e7a3319fc225f0e9584151dbea2291d" dependencies = [ - "html5ever 0.35.0", + "html5ever 0.38.0", "markup5ever_rcdom", "phf 0.13.1", ] [[package]] name = "html5ever" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" dependencies = [ "log", - "markup5ever 0.35.0", - "match_token", + "markup5ever 0.36.1", ] [[package]] name = "html5ever" -version = "0.36.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "markup5ever 0.36.1", + "markup5ever 0.38.0", ] [[package]] @@ -4541,12 +4454,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -4568,7 +4480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -4579,7 +4491,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "pin-project-lite", ] @@ -4610,31 +4522,30 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -4657,21 +4568,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.3.1", + "http 1.4.0", "hyper", "hyper-util", "log", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.7", ] [[package]] @@ -4705,25 +4615,25 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.3", "system-configuration", "tokio", + "tower-layer", "tower-service", "tracing", "windows-registry", @@ -4746,9 +4656,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -4770,12 +4680,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -4783,9 +4694,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -4796,11 +4707,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -4811,42 +4721,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -4879,9 +4785,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -4895,9 +4801,9 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "ignore" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -4911,9 +4817,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -4945,9 +4851,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "impl-more" @@ -4987,12 +4893,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -5024,7 +4930,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5065,7 +4971,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -5086,14 +4992,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] @@ -5122,21 +5028,22 @@ checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2 0.6.3", "widestring 1.2.1", - "windows-sys 0.48.0", - "winreg 0.50.0", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" dependencies = [ "serde", ] @@ -5147,16 +5054,6 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-terminal" version = "0.4.17" @@ -5212,32 +5109,81 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jni" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", ] [[package]] @@ -5252,10 +5198,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -5273,25 +5221,26 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "getrandom 0.2.16", - "hmac", + "getrandom 0.2.17", + "hmac 0.12.1", "js-sys", - "p256 0.13.2", + "p256", "p384", "pem", "rand 0.8.6", "rsa", "serde", "serde_json", - "sha2", - "signature 2.2.0", + "sha2 0.10.9", + "signature", "simple_asn1", + "zeroize", ] [[package]] @@ -5306,11 +5255,11 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.1", "libc", ] @@ -5343,19 +5292,18 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "lettre" -version = "0.11.19" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ "async-trait", "base64 0.22.1", - "chumsky", "email-encoding", "email_address", "fastrand", "futures-io", "futures-util", - "hostname 0.4.1", + "hostname", "httpdate", "idna", "mime", @@ -5364,37 +5312,31 @@ dependencies = [ "percent-encoding", "quoted_printable", "rustls", - "socket2 0.6.1", + "socket2 0.6.3", "tokio", "tokio-native-tls", "tokio-rustls", "url", - "webpki-roots 1.0.3", + "webpki-roots 1.0.7", ] -[[package]] -name = "levenshtein" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" - [[package]] name = "libbz2-rs-sys" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ "arbitrary", "cc", @@ -5402,9 +5344,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.4+1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "9b26f66f35e1871b22efcf7191564123d2a446ca0538cde63c23adfefa9b15b7" dependencies = [ "cc", "libc", @@ -5421,24 +5363,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.5", ] [[package]] @@ -5468,28 +5411,19 @@ dependencies = [ [[package]] name = "libz-ng-sys" -version = "1.1.22" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7118c2c2a3c7b6edc279a8b19507672b9c4d716f95e671172dfa4e23f9fd824" +checksum = "be734b33b7bc6a42d92d23e25e69758f866cf564a88d0bf80866fcf5a52c2255" dependencies = [ "cmake", "libc", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -5497,12 +5431,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -5511,15 +5439,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-channel" @@ -5534,14 +5462,13 @@ dependencies = [ [[package]] name = "local-ip-address" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656b3b27f8893f7bbf9485148ff9a65f019e3f33bd5cdc87c83cab16b3fd9ec8" +checksum = "aa08fb2b1ec3ea84575e94b489d06d4ce0cbf052d12acd515838f50e3c3d63e3" dependencies = [ "libc", "neli", - "thiserror 2.0.17", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5561,27 +5488,27 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lol_html" -version = "2.7.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff94cb6aef6ee52afd2c69331e9109906d855e82bd241f3110dfdf6185899ab" +checksum = "00aad58f6ec3990e795943872f13651e7a5fa59dca2c8f31a74faf8a0e0fb652" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cssparser", "encoding_rs", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.17.1", "memchr", "mime", "precomputed-hash", - "selectors", - "thiserror 2.0.17", + "selectors 0.37.0", + "thiserror 2.0.18", ] [[package]] @@ -5595,20 +5522,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.0", -] - -[[package]] -name = "lru-cache" -version = "0.1.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ - "linked-hash-map", + "hashbrown 0.16.1", ] [[package]] @@ -5640,7 +5558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" dependencies = [ "crc", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5660,6 +5578,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "serde", + "winapi 0.3.9", +] + [[package]] name = "macro_magic" version = "0.5.1" @@ -5669,7 +5598,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5683,7 +5612,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5694,7 +5623,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5705,7 +5634,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5724,16 +5653,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" -[[package]] -name = "mailchecker" -version = "6.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abad4bc63045f04cfc55aa4c55d4ec0a890c377ce56463bfc2adc2bc059c4b84" -dependencies = [ - "fast_chemail", - "once_cell", -] - [[package]] name = "maplit" version = "1.0.2" @@ -5742,55 +5661,38 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" dependencies = [ "log", - "tendril", - "web_atoms 0.1.3", + "tendril 0.4.3", + "web_atoms", ] [[package]] name = "markup5ever" -version = "0.36.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "tendril", - "web_atoms 0.2.3", + "tendril 0.5.0", + "web_atoms", ] [[package]] name = "markup5ever_rcdom" -version = "0.35.0+unofficial" +version = "0.38.0+unofficial" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bcd53df4748257345b8bc156d620340ce0f015ec1c7ef1cff475543888a31d" +checksum = "333171ccdf66e915257740d44e38ea5b1b19ce7b45d33cc35cb6f118fbd981ff" dependencies = [ - "html5ever 0.35.0", - "markup5ever 0.35.0", - "tendril", + "html5ever 0.38.0", + "markup5ever 0.38.0", + "tendril 0.5.0", "xml5ever", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "matchers" version = "0.2.0" @@ -5814,15 +5716,15 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "maxminddb" -version = "0.27.1" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99681a80368084e68fff1a4ec657b09ae6a04f1107762ec6346a82b8cc19d8eb" +checksum = "76371bd37ce742f8954daabd0fde7f1594ee43ac2200e20c003ba5c3d65e2192" dependencies = [ "ipnetwork", "log", "memchr", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5845,6 +5747,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "md5" version = "0.7.0" @@ -5853,15 +5765,15 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -5903,7 +5815,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -5924,9 +5836,9 @@ dependencies = [ [[package]] name = "minicov" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" dependencies = [ "cc", "walkdir", @@ -5934,9 +5846,9 @@ dependencies = [ [[package]] name = "minidump" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9ea21482e519a57bfc5df90b736f25465ef349a31c18ff2c6332a2f18474de" +checksum = "a902ca21d9772a66d3d1b050b3436dcadb192694be01409e0219902dcf4bc1e8" dependencies = [ "debugid", "encoding_rs", @@ -5947,7 +5859,7 @@ dependencies = [ "prost 0.13.5", "range-map", "scroll", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -5955,11 +5867,11 @@ dependencies = [ [[package]] name = "minidump-common" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1e7ee92185b2f4fa67c3e5c1743057d979e145f58b4391d5481b79f0d8067c" +checksum = "2e16d10087ae9e375bad7a40e8ef5504bc08e808ccc6019067ff9de42a84570f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "debugid", "num-derive", "num-traits", @@ -5992,19 +5904,19 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -6046,7 +5958,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -6058,25 +5970,26 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "mockito" -version = "1.7.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" dependencies = [ "assert-json-diff", "bytes", - "colored 3.0.0", - "futures-util", - "http 1.3.1", + "colored 3.1.1", + "futures-core", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", "hyper-util", "log", + "pin-project-lite", "rand 0.9.4", "regex", "serde_json", @@ -6087,9 +6000,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.12" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -6104,9 +6017,9 @@ dependencies = [ [[package]] name = "mongocrypt" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22426d6318d19c5c0773f783f85375265d6a8f0fa76a733da8dc4355516ec63d" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" dependencies = [ "bson", "mongocrypt-sys", @@ -6116,70 +6029,66 @@ dependencies = [ [[package]] name = "mongocrypt-sys" -version = "0.1.4+1.12.0" +version = "0.1.5+1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda42df21d035f88030aad8e877492fac814680e1d7336a57b2a091b989ae388" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" [[package]] name = "mongodb" -version = "3.3.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622f272c59e54a3c85f5902c6b8e7b1653a6b6681f45e4c42d6581301119a4b8" +checksum = "1ef2c933617431ad0246fb5b43c425ebdae18c7f7259c87de0726d93b0e7e91b" dependencies = [ - "async-trait", - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.22.1", + "bitflags 2.11.1", "bson", - "chrono", "derive-where", - "derive_more 0.99.20", + "derive_more", "futures-core", - "futures-executor", "futures-io", "futures-util", "hex", - "hickory-proto 0.24.4", - "hickory-resolver 0.24.4", - "hmac", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", + "hmac 0.12.1", "macro_magic", - "md-5", + "md-5 0.10.6", "mongocrypt", "mongodb-internal-macros", - "once_cell", - "pbkdf2 0.11.0", + "pbkdf2", "percent-encoding", - "rand 0.8.6", + "rand 0.9.4", "rustc_version_runtime", "rustls", "rustversion", "serde", "serde_bytes", - "serde_with 3.15.1", + "serde_with 3.20.0", "sha1 0.10.6", - "sha2", - "socket2 0.5.10", + "sha2 0.10.9", + "socket2 0.6.3", "stringprep", "strsim 0.11.1", "take_mut", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-rustls", "tokio-util", "typed-builder", "uuid", - "webpki-roots 0.26.11", + "webpki-roots 1.0.7", ] [[package]] name = "mongodb-internal-macros" -version = "3.3.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63981427a0f26b89632fd2574280e069d09fb2912a3138da15de0174d11dd077" +checksum = "9e5758dc828eb2d02ec30563cba365609d56ddd833190b192beaee2b475a7bb3" dependencies = [ "macro_magic", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -6201,14 +6110,14 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "moxcms" -version = "0.7.7" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -6223,7 +6132,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.3.1", + "http 1.4.0", "httparse", "memchr", "mime", @@ -6239,44 +6148,54 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "neli" -version = "0.6.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" dependencies = [ + "bitflags 2.11.1", "byteorder", + "derive_builder 0.20.2", + "getset", "libc", "log", "neli-proc-macros", + "parking_lot", ] [[package]] name = "neli-proc-macros" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" dependencies = [ "either", "proc-macro2", "quote", "serde", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -6324,11 +6243,11 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "log", "netlink-packet-core 0.8.1", @@ -6354,14 +6273,14 @@ checksum = "3176f18d11a1ae46053e59ec89d46ba318ae1343615bd3f8c908bfc84edae35c" dependencies = [ "byteorder", "pastey", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "netlink-packet-wireguard" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037892b0e01ce41f30398a47be2051e712a2cf1eed9cb7e5e6a92b05c423255b" +checksum = "205d2bad950c9cbbbf08cc5432d6501edfe02d3a34ecad822a3e91c98e97dbf6" dependencies = [ "libc", "log", @@ -6380,7 +6299,7 @@ dependencies = [ "log", "netlink-packet-core 0.7.0", "netlink-sys", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -6402,15 +6321,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - [[package]] name = "nix" version = "0.24.3" @@ -6429,7 +6339,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "libc", ] @@ -6440,7 +6350,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -6449,11 +6359,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -6500,6 +6410,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "node-semver" version = "2.2.0" @@ -6544,7 +6463,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossbeam-channel", "filetime", "fsevent-sys", @@ -6559,9 +6478,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi 0.3.9", ] @@ -6626,9 +6545,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -6638,7 +6557,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -6699,23 +6618,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "oauth2" -version = "5.0.0" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "base64 0.22.1", - "chrono", - "getrandom 0.2.16", - "http 1.3.1", - "rand 0.8.6", - "reqwest", - "serde", - "serde_json", - "serde_path_to_error", - "sha2", - "thiserror 1.0.69", - "url", + "bitflags 2.11.1", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", ] [[package]] @@ -6729,9 +6646,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.49.5" +version = "0.49.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89f6f72d7084a80bf261bb6b6f83bd633323d5633d5ec7988c6c95b20448b2b5" +checksum = "4ddbc3bb87e8c680febf16f56855bbd8b44a38e18c913334213ab34908e71a09" dependencies = [ "arc-swap", "async-trait", @@ -6743,8 +6660,8 @@ dependencies = [ "either", "futures", "futures-util", - "getrandom 0.2.16", - "http 1.3.1", + "getrandom 0.2.17", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -6762,7 +6679,7 @@ dependencies = [ "serde_urlencoded", "snafu", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "url", @@ -6784,14 +6701,14 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -6805,11 +6722,11 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" -version = "6.5.1" +version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "once_cell", "onig_sys", @@ -6817,14 +6734,20 @@ dependencies = [ [[package]] name = "onig_sys" -version = "69.9.1" +version = "69.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" dependencies = [ "cc", "pkg-config", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -6833,11 +6756,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", @@ -6853,7 +6776,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -6862,20 +6785,26 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-src" -version = "300.5.4+3.5.4" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -6894,7 +6823,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -6922,7 +6851,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand 0.9.4", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -6971,7 +6900,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -6980,27 +6909,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2", -] - [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -7009,10 +6927,10 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -7039,9 +6957,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -7066,7 +6984,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -7104,15 +7022,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "pbkdf2" version = "0.12.2" @@ -7120,7 +7029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -7150,9 +7059,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -7160,9 +7069,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.3" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -7170,25 +7079,25 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.3" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "pest_meta" -version = "2.8.3" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -7198,7 +7107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.0", + "indexmap 2.14.0", ] [[package]] @@ -7210,15 +7119,6 @@ dependencies = [ "serde", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.12.1" @@ -7239,16 +7139,6 @@ dependencies = [ "serde", ] -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf_codegen" version = "0.12.1" @@ -7269,16 +7159,6 @@ dependencies = [ "phf_shared 0.13.1", ] -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", -] - [[package]] name = "phf_generator" version = "0.12.1" @@ -7309,16 +7189,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.108", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", + "syn 2.0.117", ] [[package]] @@ -7341,29 +7212,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -7398,7 +7269,7 @@ dependencies = [ "cf-rustracing", "cf-rustracing-jaeger", "hex", - "http 1.3.1", + "http 1.4.0", "httparse", "httpdate", "indexmap 1.9.3", @@ -7439,15 +7310,15 @@ dependencies = [ "derivative", "flate2", "futures", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "httparse", "httpdate", "libc", "log", "nix 0.24.3", "once_cell", - "openssl-probe", + "openssl-probe 0.1.6", "parking_lot", "percent-encoding", "pingora-error", @@ -7462,7 +7333,7 @@ dependencies = [ "serde", "serde_yaml", "sfv", - "socket2 0.6.1", + "socket2 0.6.3", "strum", "strum_macros", "tokio", @@ -7486,7 +7357,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2705feb8b50d4e734e0c7d3879aa040e655a45656276323ff530e254585dd816" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", "httparse", "pingora-error", "pingora-http", @@ -7502,7 +7373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb52d4651b687fab6abf669539cfd97b7cd94b301fde8f57c63354f9c9cc5e2" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", "pingora-error", ] @@ -7526,7 +7397,7 @@ dependencies = [ "derivative", "fnv", "futures", - "http 1.3.1", + "http 1.4.0", "log", "pingora-core", "pingora-error", @@ -7544,7 +7415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91bb5030596a3d442c0866ac68afe29c14ba558e77c726dcdf7016b0dbb359d9" dependencies = [ "arrayvec", - "hashbrown 0.16.0", + "hashbrown 0.17.1", "parking_lot", "rand 0.8.6", ] @@ -7587,8 +7458,8 @@ dependencies = [ "bytes", "clap", "futures", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "log", "once_cell", "pingora-cache", @@ -7631,19 +7502,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", + "der", + "pkcs8", + "spki", ] [[package]] @@ -7652,15 +7513,15 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", ] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -7670,11 +7531,11 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -7712,9 +7573,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -7730,27 +7591,27 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", - "hmac", - "md-5", + "hmac 0.13.0", + "md-5 0.11.0", "memchr", - "rand 0.9.4", - "sha2", + "rand 0.10.1", + "sha2 0.11.0", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" dependencies = [ "bytes", "chrono", @@ -7763,9 +7624,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -7778,9 +7639,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppmd-rust" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" [[package]] name = "ppv-lite86" @@ -7799,9 +7660,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "predicates-core", @@ -7809,15 +7670,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -7825,10 +7686,11 @@ dependencies = [ [[package]] name = "prefix-trie" -version = "0.7.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" dependencies = [ + "either", "ipnet", "num-traits", ] @@ -7850,7 +7712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -7859,16 +7721,16 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve 0.13.8", + "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -7890,14 +7752,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -7910,7 +7772,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "version_check", "yansi", ] @@ -7921,27 +7783,27 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hex", ] [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -7971,12 +7833,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.1", + "prost-derive 0.14.3", ] [[package]] @@ -7995,7 +7857,7 @@ dependencies = [ "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.108", + "syn 2.0.117", "tempfile", ] @@ -8009,20 +7871,20 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "prost-derive" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -8036,11 +7898,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost 0.14.1", + "prost 0.14.3", ] [[package]] @@ -8049,15 +7911,6 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -[[package]] -name = "psm" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" -dependencies = [ - "cc", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -8084,7 +7937,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -8099,12 +7952,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" -dependencies = [ - "num-traits", -] +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -8151,10 +8001,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.3", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -8171,11 +8021,11 @@ dependencies = [ "lru-slab", "rand 0.9.4", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -8190,25 +8040,25 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" [[package]] name = "r-efi" @@ -8228,16 +8078,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.4.6" @@ -8269,7 +8109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -8300,7 +8140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -8324,14 +8164,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -8353,19 +8193,21 @@ dependencies = [ [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -8374,23 +8216,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.6", - "rand_chacha 0.3.1", + "rand 0.9.4", + "rand_chacha 0.9.0", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -8403,9 +8243,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -8511,7 +8351,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.11.1", ] [[package]] @@ -8520,7 +8369,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -8531,9 +8380,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -8553,14 +8402,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -8570,9 +8419,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -8581,15 +8430,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relay-base-schema" @@ -8600,7 +8449,7 @@ dependencies = [ "relay-common", "relay-protocol", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -8639,7 +8488,7 @@ source = "git+https://github.com/getsentry/relay?rev=ca7e20d#ca7e20d0a7e27d2029c dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] @@ -8663,7 +8512,7 @@ dependencies = [ "sentry-release-parser", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "uuid", ] @@ -8689,7 +8538,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicase", "uuid", ] @@ -8701,7 +8550,7 @@ source = "git+https://github.com/getsentry/relay?rev=ca7e20d#ca7e20d0a7e27d2029c dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] @@ -8731,17 +8580,17 @@ checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -8766,7 +8615,7 @@ dependencies = [ "tokio-native-tls", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -8774,50 +8623,39 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.3", -] - -[[package]] -name = "reserve-port" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356" -dependencies = [ - "thiserror 2.0.17", + "webpki-roots 1.0.7", ] [[package]] -name = "resolv-conf" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" - -[[package]] -name = "rfc6979" -version = "0.3.1" +name = "reserve-port" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", + "thiserror 2.0.18", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "ring" @@ -8827,7 +8665,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -8862,71 +8700,21 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rmcp" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ab0892f4938752b34ae47cb53910b1b0921e55e77ddb6e44df666cab17939f" -dependencies = [ - "axum 0.8.6", - "base64 0.22.1", - "bytes", - "chrono", - "futures", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "oauth2", - "paste", - "pin-project-lite", - "rand 0.9.4", - "reqwest", - "rmcp-macros", - "schemars 1.0.4", - "serde", - "serde_json", - "sse-stream", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio-util", - "tower-service", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "rmcp-macros" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1827cd98dab34cade0513243c6fe0351f0f0b2c9d6825460bcf45b42804bdda0" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.108", -] - [[package]] name = "rmp" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" dependencies = [ - "byteorder", "num-traits", - "paste", ] [[package]] name = "rmp-serde" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" dependencies = [ - "byteorder", "rmp", "serde", ] @@ -8938,7 +8726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.10.0", + "bitflags 2.11.1", "serde", "serde_derive", ] @@ -8955,10 +8743,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8 0.10.2", + "pkcs8", "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", + "signature", + "spki", "subtle", "zeroize", ] @@ -8983,9 +8771,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.8.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -8994,24 +8782,24 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.8.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.108", + "syn 2.0.117", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.8.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -9050,17 +8838,17 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "mime", "rand 0.9.4", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" dependencies = [ "arrayvec", "borsh", @@ -9070,13 +8858,14 @@ dependencies = [ "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -9086,9 +8875,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -9124,7 +8913,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9133,22 +8922,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", @@ -9162,14 +8951,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -9183,9 +8972,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -9211,9 +9000,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -9245,9 +9034,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -9260,7 +9049,7 @@ checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", - "schemars_derive 0.8.22", + "schemars_derive", "serde", "serde_json", ] @@ -9279,14 +9068,12 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ - "chrono", "dyn-clone", "ref-cast", - "schemars_derive 1.0.4", "serde", "serde_json", ] @@ -9300,19 +9087,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.108", -] - -[[package]] -name = "schemars_derive" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9332,8 +9107,8 @@ dependencies = [ "getopts", "html5ever 0.36.1", "precomputed-hash", - "selectors", - "tendril", + "selectors 0.33.0", + "tendril 0.4.3", ] [[package]] @@ -9353,7 +9128,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9372,22 +9147,23 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "sea-orm" -version = "1.1.17" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699b1ec145a6530c8f862eed7529d8a6068392e628d81cc70182934001e9c2a3" +checksum = "2dc312fedd460a47ea563911761d254a84e7b51d8cc73ec92c929e78f33fa957" dependencies = [ "async-stream", "async-trait", "bigdecimal", "chrono", - "derive_more 2.0.1", + "derive_more", "futures-util", "log", + "mac_address", "ouroboros", "pgvector", "rust_decimal", @@ -9398,7 +9174,7 @@ dependencies = [ "serde_json", "sqlx", "strum", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "url", @@ -9407,9 +9183,9 @@ dependencies = [ [[package]] name = "sea-orm-cli" -version = "1.1.17" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cd31ebb07814d4c7b73796708bfab6c13d22f8db072cdb5115f967f4d5d2c" +checksum = "da80ebcdb44571e86f03a2bdcb5532136a87397f366f38bbce64673fc5e6a450" dependencies = [ "chrono", "clap", @@ -9426,23 +9202,23 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.17" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c964f4b7f34f53decf381bc88f03187b9355e07f356ce65544626e781a9585" +checksum = "9b9a3f90e336ec74803e8eb98c61bc98754c1adfba3b4f84d946237b752b1c88" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", "sea-bae", - "syn 2.0.108", + "syn 2.0.117", "unicode-ident", ] [[package]] name = "sea-orm-migration" -version = "1.1.17" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977e3f71486b04371026d1ecd899f49cf437f832cd11d463f8948ee02e47ed9e" +checksum = "07c577f2959277e936c1d08109acd1e08fc36a95ef29ec028190ba82cad8f96e" dependencies = [ "async-trait", "clap", @@ -9497,8 +9273,8 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.108", - "thiserror 2.0.17", + "syn 2.0.117", + "thiserror 2.0.18", ] [[package]] @@ -9523,7 +9299,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9540,21 +9316,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", + "syn 2.0.117", ] [[package]] @@ -9563,10 +9325,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", + "base16ct", + "der", "generic-array", - "pkcs8 0.10.2", + "pkcs8", "subtle", "zeroize", ] @@ -9582,64 +9344,70 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", + "bitflags 2.11.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", ] [[package]] -name = "security-framework" -version = "3.5.1" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", "core-foundation-sys", "libc", - "security-framework-sys", ] [[package]] -name = "security-framework-sys" -version = "2.15.0" +name = "selectors" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" dependencies = [ - "core-foundation-sys", - "libc", + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash 2.1.2", + "servo_arc", + "smallvec", ] [[package]] name = "selectors" -version = "0.33.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" +checksum = "2cfaaa6035167f0e604e42723c7650d59ee269ef220d7bbe0565602c8a0173b9" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cssparser", - "derive_more 2.0.1", + "derive_more", "log", "new_debug_unreachable", "phf 0.13.1", "phf_codegen 0.13.1", "precomputed-hash", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "servo_arc", "smallvec", ] [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -9647,9 +9415,9 @@ dependencies = [ [[package]] name = "sentry-release-parser" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "663a866435288420b8af1ecd0fe5774c4d5f0f584eadb2456303d8ab0f5c8313" +checksum = "c98f1c38deb5d037aacfcbf6790951da8acba07b7e3a3c8b90df531f2d4ebae2" dependencies = [ "lazy_static", "regex", @@ -9666,7 +9434,7 @@ dependencies = [ "rand 0.9.4", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -9709,7 +9477,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9720,21 +9488,21 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -9756,7 +9524,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9768,6 +9536,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -9792,20 +9569,21 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.14.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.2.1", "serde_core", "serde_json", - "serde_with_macros 3.15.1", + "serde_with_macros 3.20.0", "time", ] @@ -9818,19 +9596,19 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9839,7 +9617,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -9848,11 +9626,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -9862,13 +9641,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -9887,7 +9666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.14.0", "rust_decimal", ] @@ -9910,7 +9689,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -9930,6 +9709,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -9947,23 +9737,14 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "2.2.0" @@ -9976,9 +9757,19 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simd_helpers" @@ -10003,27 +9794,27 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slug" @@ -10052,7 +9843,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10079,7 +9870,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10094,12 +9885,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10124,7 +9915,7 @@ dependencies = [ "data-encoding", "debugid", "if_chain", - "rustc-hash 2.1.1", + "rustc-hash 2.1.2", "serde", "serde_json", "unicode-id-start", @@ -10146,16 +9937,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - [[package]] name = "spki" version = "0.7.3" @@ -10163,7 +9944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.10", + "der", ] [[package]] @@ -10211,7 +9992,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink 0.10.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "memchr", "native-tls", @@ -10221,9 +10002,9 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-stream", @@ -10243,7 +10024,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10261,12 +10042,12 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.108", + "syn 2.0.117", "tokio", "url", ] @@ -10280,7 +10061,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.10.0", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -10295,10 +10076,10 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", @@ -10307,15 +10088,15 @@ dependencies = [ "rust_decimal", "serde", "sha1 0.10.6", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", + "whoami 1.6.1", ] [[package]] @@ -10327,7 +10108,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.10.0", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -10338,11 +10119,11 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "num-bigint", "once_cell", @@ -10350,15 +10131,15 @@ dependencies = [ "rust_decimal", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", + "whoami 1.6.1", ] [[package]] @@ -10381,64 +10162,25 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "url", "uuid", ] -[[package]] -name = "sse-stream" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" -dependencies = [ - "bytes", - "futures-util", - "http-body 1.0.1", - "http-body-util", - "pin-project-lite", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.11.3", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -10449,18 +10191,7 @@ dependencies = [ "parking_lot", "phf_shared 0.13.1", "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", + "serde", ] [[package]] @@ -10507,7 +10238,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10518,7 +10249,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10540,7 +10271,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10562,9 +10293,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -10588,7 +10319,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -10622,11 +10353,11 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -10641,19 +10372,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -10674,21 +10392,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", "xattr", ] -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "temp-dir" version = "0.1.16" @@ -10707,27 +10419,27 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] [[package]] name = "temps-agent" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.9", "bollard", "bytes", "futures", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "hyper-util", @@ -10737,16 +10449,16 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sysinfo 0.29.11", "tempfile", "temps-core", "temps-deployer", "temps-dns-resolver", "temps-network", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "utoipa", @@ -10756,12 +10468,12 @@ dependencies = [ [[package]] name = "temps-agents" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-stream", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "bollard", "bytes", @@ -10770,7 +10482,7 @@ dependencies = [ "flate2", "futures", "hex", - "http 1.3.1", + "http 1.4.0", "http-body-util", "libc", "pulldown-cmark", @@ -10791,7 +10503,7 @@ dependencies = [ "temps-error-tracking", "temps-git", "temps-notifications", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -10800,7 +10512,7 @@ dependencies = [ [[package]] name = "temps-agents-mcp-proxy" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "serde", "serde_json", @@ -10808,16 +10520,16 @@ dependencies = [ [[package]] name = "temps-ai-gateway" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-stream", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "bytes", "chrono", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "reqwest", @@ -10827,10 +10539,10 @@ dependencies = [ "temps-auth", "temps-core", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tracing", "utoipa", "uuid", @@ -10838,11 +10550,11 @@ dependencies = [ [[package]] name = "temps-analytics" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "chrono", "rand 0.8.6", @@ -10856,7 +10568,7 @@ dependencies = [ "temps-entities", "temps-migrations", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -10868,7 +10580,7 @@ dependencies = [ [[package]] name = "temps-analytics-backend" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "chrono", @@ -10877,7 +10589,7 @@ dependencies = [ "serde", "serde_json", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "uuid", @@ -10885,11 +10597,11 @@ dependencies = [ [[package]] name = "temps-analytics-events" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "chrono", "clickhouse", "sea-orm", @@ -10906,9 +10618,9 @@ dependencies = [ "temps-migrations", "temps-proxy", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "utoipa", @@ -10918,11 +10630,11 @@ dependencies = [ [[package]] name = "temps-analytics-funnels" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "chrono", "sea-orm", "serde", @@ -10931,7 +10643,7 @@ dependencies = [ "temps-core", "temps-database", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -10939,10 +10651,10 @@ dependencies = [ [[package]] name = "temps-analytics-performance" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "chrono", "lazy_static", "maplit", @@ -10962,12 +10674,12 @@ dependencies = [ [[package]] name = "temps-analytics-session-replay" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", - "axum-test 18.1.0", + "axum 0.8.9", + "axum-test 18.7.0", "base64 0.22.1", "chrono", "flate2", @@ -10979,7 +10691,7 @@ dependencies = [ "temps-database", "temps-entities", "temps-routes", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -10988,10 +10700,10 @@ dependencies = [ [[package]] name = "temps-audit" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "chrono", "log", "sea-orm", @@ -11009,12 +10721,12 @@ dependencies = [ [[package]] name = "temps-auth" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "argon2", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "base32", "base64 0.22.1", @@ -11028,16 +10740,16 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.9", "temps-core", "temps-database", "temps-entities", "temps-migrations", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "totp-rs", - "tower 0.5.2", + "tower 0.5.3", "tower-cookies", "tracing", "urlencoding", @@ -11047,14 +10759,14 @@ dependencies = [ [[package]] name = "temps-backup" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-stream", "async-trait", "aws-sdk-s3", "aws-smithy-http-client", - "axum 0.8.6", + "axum 0.8.9", "bollard", "chrono", "cron 0.15.0", @@ -11077,7 +10789,7 @@ dependencies = [ "temps-notifications", "temps-providers", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -11091,7 +10803,7 @@ dependencies = [ [[package]] name = "temps-backup-core" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "chrono", @@ -11101,7 +10813,7 @@ dependencies = [ "serde_json", "temps-core", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -11111,19 +10823,19 @@ dependencies = [ [[package]] name = "temps-blob" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", "aws-config", "aws-sdk-s3", - "axum 0.8.6", + "axum 0.8.9", "bollard", "bytes", "chrono", "futures", "futures-util", - "http 1.3.1", + "http 1.4.0", "once_cell", "rand 0.8.6", "reqwest", @@ -11136,10 +10848,10 @@ dependencies = [ "temps-database", "temps-entities", "temps-providers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tracing", "urlencoding", "utoipa", @@ -11150,26 +10862,25 @@ dependencies = [ name = "temps-captcha-wasm" version = "0.1.0" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "hex", "js-sys", - "sha2", + "sha2 0.10.9", "wasm-bindgen", "wasm-bindgen-test", ] [[package]] name = "temps-cli" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "argon2", "async-trait", "aws-sdk-s3", - "axum 0.8.6", + "axum 0.8.9", "bollard", "bytes", - "check-if-email-exists", "chrono", "clap", "colored 2.2.0", @@ -11190,7 +10901,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "temps-agent", "temps-agents", @@ -11243,7 +10954,7 @@ dependencies = [ "temps-vulnerability-scanner", "temps-webhooks", "temps-wireguard", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -11258,10 +10969,10 @@ dependencies = [ [[package]] name = "temps-config" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", "config", @@ -11273,12 +10984,12 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-core", "temps-database", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -11288,31 +10999,31 @@ dependencies = [ [[package]] name = "temps-core" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "aes-gcm", "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", "cookie 0.18.1", "futures", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "log", "once_cell", "rand 0.8.6", "serde", "serde_json", "serde_yaml", - "sha2", + "sha2 0.10.9", "tempfile", "temps-memory", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "url", @@ -11324,7 +11035,7 @@ dependencies = [ [[package]] name = "temps-database" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "futures", @@ -11342,7 +11053,7 @@ dependencies = [ [[package]] name = "temps-deployer" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", @@ -11366,7 +11077,7 @@ dependencies = [ "temps-config", "temps-core", "temps-network", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tokio-util", @@ -11378,17 +11089,17 @@ dependencies = [ [[package]] name = "temps-deployments" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "bollard", "bytes", "chrono", "cron 0.12.1", - "env_logger 0.11.8", + "env_logger 0.11.10", "flate2", "futures", "futures-util", @@ -11404,7 +11115,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_yaml", - "sha2", + "sha2 0.10.9", "tar", "tempfile", "temps-auth", @@ -11426,12 +11137,12 @@ dependencies = [ "temps-screenshots", "temps-vulnerability-scanner", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tokio-tungstenite 0.24.0", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "urlencoding", @@ -11442,18 +11153,18 @@ dependencies = [ [[package]] name = "temps-dns" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "base64 0.22.1", "chrono", "cloudflare 0.14.0", "futures", "hex", - "hmac", + "hmac 0.12.1", "mockall 0.13.1", "quick-xml", "reqwest", @@ -11461,14 +11172,14 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-core", "temps-database", "temps-entities", "temps-migrations", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -11480,20 +11191,19 @@ dependencies = [ [[package]] name = "temps-dns-resolver" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", "chrono", - "hickory-client", - "hickory-proto 0.25.2", - "hickory-resolver 0.25.2", + "hickory-proto 0.26.1", + "hickory-resolver 0.26.1", "hickory-server", "reqwest", "serde", "serde_json", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -11502,16 +11212,16 @@ dependencies = [ [[package]] name = "temps-domains" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-test 16.4.1", "base64 0.22.1", "chrono", "cloudflare 0.14.1", - "hickory-resolver 0.25.2", + "hickory-resolver 0.26.1", "instant-acme", "log", "rcgen", @@ -11522,7 +11232,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-config", "temps-core", @@ -11530,7 +11240,7 @@ dependencies = [ "temps-dns", "temps-entities", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-util", @@ -11538,16 +11248,16 @@ dependencies = [ "utoipa", "uuid", "webpki-roots 0.26.11", - "x509-parser 0.18.0", + "x509-parser 0.18.1", ] [[package]] name = "temps-edge" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "aes-gcm", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -11555,7 +11265,7 @@ dependencies = [ "flate2", "gethostname", "hkdf", - "http 1.3.1", + "http 1.4.0", "http-body-util", "pingora", "pingora-core", @@ -11569,14 +11279,14 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "temps-core", "temps-file-store", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "x25519-dalek", @@ -11584,20 +11294,20 @@ dependencies = [ [[package]] name = "temps-email" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", "aws-config", "aws-sdk-sesv2", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", - "check-if-email-exists", "chrono", "futures", - "hickory-resolver 0.24.4", - "http 1.3.1", + "hickory-resolver 0.26.1", + "http 1.4.0", "http-body-util", + "md5", "reqwest", "sea-orm", "serde", @@ -11610,10 +11320,11 @@ dependencies = [ "temps-entities", "temps-migrations", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", + "tokio-socks", "tokio-test", - "tower 0.5.2", + "tower 0.5.3", "tracing", "urlencoding", "utoipa", @@ -11622,20 +11333,20 @@ dependencies = [ [[package]] name = "temps-email-tracking" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", "hex", - "hmac", + "hmac 0.12.1", "lol_html", "reqwest", "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-config", "temps-core", @@ -11643,7 +11354,7 @@ dependencies = [ "temps-email", "temps-entities", "temps-migrations", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -11655,10 +11366,10 @@ dependencies = [ [[package]] name = "temps-embeddings" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "tiktoken-rs", "tokenizers", "tokio", @@ -11666,30 +11377,30 @@ dependencies = [ [[package]] name = "temps-entities" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "chrono", "sea-orm", "serde", "serde_json", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "utoipa", "uuid", ] [[package]] name = "temps-environments" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "argon2", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "chrono", "futures", "futures-util", - "http 1.3.1", + "http 1.4.0", "log", "sea-orm", "serde", @@ -11701,7 +11412,7 @@ dependencies = [ "temps-core", "temps-database", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -11709,19 +11420,19 @@ dependencies = [ [[package]] name = "temps-error-tracking" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", - "axum-test 18.1.0", + "axum-test 18.7.0", "chrono", "debugid", "flate2", "futures", "hex", - "http 1.3.1", + "http 1.4.0", "log", "rand 0.8.6", "regex", @@ -11733,7 +11444,7 @@ dependencies = [ "serde_derive", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "sourcemap", "temps-auth", "temps-config", @@ -11745,7 +11456,7 @@ dependencies = [ "temps-migrations", "temps-notifications", "temps-projects", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "tokio-test", @@ -11760,7 +11471,7 @@ dependencies = [ name = "temps-example-plugin" version = "0.1.0" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "chrono", "include_dir", "mime_guess", @@ -11771,7 +11482,7 @@ dependencies = [ "serde_json", "tempfile", "temps-plugin-sdk", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -11781,9 +11492,9 @@ dependencies = [ [[package]] name = "temps-external-plugins" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "chrono", "futures", "hyper", @@ -11798,7 +11509,7 @@ dependencies = [ "temps-entities", "tokio", "tokio-tungstenite 0.28.0", - "tower 0.5.2", + "tower 0.5.3", "tracing", "utoipa", "uuid", @@ -11806,13 +11517,13 @@ dependencies = [ [[package]] name = "temps-file-store" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "bytes", - "sha2", + "sha2 0.10.9", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -11821,10 +11532,10 @@ dependencies = [ [[package]] name = "temps-geo" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "chrono", "log", "maxminddb", @@ -11834,7 +11545,7 @@ dependencies = [ "temps-core", "temps-database", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -11842,11 +11553,11 @@ dependencies = [ [[package]] name = "temps-git" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -11855,7 +11566,7 @@ dependencies = [ "futures-util", "git2", "hex", - "hmac", + "hmac 0.12.1", "http-body-util", "hyper", "jsonwebtoken", @@ -11869,7 +11580,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.9", "tar", "tempfile", "temps-auth", @@ -11879,7 +11590,7 @@ dependencies = [ "temps-entities", "temps-presets", "temps-queue", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -11890,7 +11601,7 @@ dependencies = [ [[package]] name = "temps-git-credential" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "chrono", "nix 0.29.0", @@ -11898,7 +11609,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "tracing-subscriber", @@ -11908,7 +11619,7 @@ dependencies = [ name = "temps-google-indexing-plugin" version = "0.1.0" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", "include_dir", @@ -11922,7 +11633,7 @@ dependencies = [ "serde_json", "tempfile", "temps-plugin-sdk", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -11931,10 +11642,10 @@ dependencies = [ [[package]] name = "temps-import" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.9", "chrono", "sea-orm", "serde", @@ -11949,7 +11660,7 @@ dependencies = [ "temps-import-types", "temps-presets", "temps-projects", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -11958,7 +11669,7 @@ dependencies = [ [[package]] name = "temps-import-docker" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "bollard", @@ -11974,14 +11685,14 @@ dependencies = [ "temps-import-types", "temps-presets", "temps-projects", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "temps-import-types" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "chrono", @@ -11997,7 +11708,7 @@ dependencies = [ name = "temps-indexnow-plugin" version = "0.1.0" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "chrono", "include_dir", "mime_guess", @@ -12008,7 +11719,7 @@ dependencies = [ "serde_json", "tempfile", "temps-plugin-sdk", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -12018,14 +11729,14 @@ dependencies = [ [[package]] name = "temps-infra" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "bollard", - "env_logger 0.11.8", + "env_logger 0.11.10", "get_if_addrs", - "hickory-resolver 0.24.4", + "hickory-resolver 0.26.1", "log", "parking_lot", "reqwest", @@ -12035,23 +11746,23 @@ dependencies = [ "temps-core", "tokio", "tokio-test", - "tower 0.5.2", + "tower 0.5.3", "tracing", "utoipa", ] [[package]] name = "temps-kv" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "bollard", "chrono", "futures", "futures-util", - "http 1.3.1", + "http 1.4.0", "once_cell", "rand 0.8.6", "redis 0.28.2", @@ -12064,9 +11775,9 @@ dependencies = [ "temps-database", "temps-entities", "temps-providers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "tower 0.5.2", + "tower 0.5.3", "tracing", "urlencoding", "utoipa", @@ -12077,7 +11788,7 @@ dependencies = [ name = "temps-lighthouse-plugin" version = "0.1.0" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "chrono", "include_dir", "mime_guess", @@ -12086,7 +11797,7 @@ dependencies = [ "serde_json", "tempfile", "temps-plugin-sdk", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -12095,12 +11806,12 @@ dependencies = [ [[package]] name = "temps-log-aggregator" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "aws-sdk-s3", - "axum 0.8.6", - "axum-test 18.1.0", + "axum 0.8.9", + "axum-test 18.7.0", "bollard", "bytes", "chrono", @@ -12112,13 +11823,13 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "tempfile", "temps-auth", "temps-core", "temps-database", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -12130,7 +11841,7 @@ dependencies = [ [[package]] name = "temps-logs" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-stream", "bollard", @@ -12142,43 +11853,24 @@ dependencies = [ "serde_json", "tempfile", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", ] -[[package]] -name = "temps-mcp" -version = "0.1.0-beta.15" -dependencies = [ - "anyhow", - "rmcp", - "serde", - "serde_json", - "temps-auth", - "temps-config", - "temps-core", - "temps-database", - "temps-domains", - "temps-entities", - "temps-projects", - "tokio", - "tracing", -] - [[package]] name = "temps-memory" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] [[package]] name = "temps-migrations" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "sea-orm", @@ -12191,7 +11883,7 @@ dependencies = [ [[package]] name = "temps-monitoring" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", @@ -12207,7 +11899,7 @@ dependencies = [ "temps-database", "temps-deployer", "temps-entities", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "uuid", @@ -12215,7 +11907,7 @@ dependencies = [ [[package]] name = "temps-network" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "bollard", @@ -12236,7 +11928,7 @@ dependencies = [ "temps-entities", "temps-migrations", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "uuid", @@ -12244,11 +11936,11 @@ dependencies = [ [[package]] name = "temps-notifications" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "chrono", "futures-util", @@ -12263,7 +11955,7 @@ dependencies = [ "temps-database", "temps-entities", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tower 0.4.13", @@ -12274,14 +11966,14 @@ dependencies = [ [[package]] name = "temps-observability" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", - "http 1.3.1", + "http 1.4.0", "sea-orm", "sea-orm-migration", "serde", @@ -12291,7 +11983,7 @@ dependencies = [ "temps-database", "temps-entities", "temps-migrations", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -12301,18 +11993,18 @@ dependencies = [ [[package]] name = "temps-otel" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "aws-config", "aws-sdk-s3", - "axum 0.8.6", + "axum 0.8.9", "bytes", "chrono", "flate2", "futures", "hex", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "prost 0.13.5", @@ -12320,7 +12012,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-config", "temps-core", @@ -12328,10 +12020,10 @@ dependencies = [ "temps-deployer", "temps-entities", "temps-migrations", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tracing", "utoipa", @@ -12341,9 +12033,9 @@ dependencies = [ [[package]] name = "temps-plugin-sdk" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "clap", "flate2", "futures", @@ -12353,11 +12045,11 @@ dependencies = [ "serde_json", "tar", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-tungstenite 0.28.0", - "tower 0.5.2", + "tower 0.5.3", "tracing", "tracing-subscriber", "utoipa", @@ -12366,7 +12058,7 @@ dependencies = [ [[package]] name = "temps-presets" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", @@ -12385,7 +12077,7 @@ dependencies = [ [[package]] name = "temps-preview-gateway" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "tokio", @@ -12395,23 +12087,23 @@ dependencies = [ [[package]] name = "temps-projects" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "bollard", "chrono", "flate2", "futures", "futures-util", - "http 1.3.1", + "http 1.4.0", "log", "sea-orm", "serde", "serde_derive", "serde_json", - "sha2", + "sha2 0.10.9", "slug", "tempfile", "temps-auth", @@ -12424,7 +12116,7 @@ dependencies = [ "temps-presets", "temps-providers", "temps-queue", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -12433,13 +12125,13 @@ dependencies = [ [[package]] name = "temps-providers" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", "aws-sdk-s3", "aws-smithy-runtime", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "base64 0.22.1", "bollard", @@ -12447,7 +12139,7 @@ dependencies = [ "chrono", "flate2", "futures", - "http 1.3.1", + "http 1.4.0", "hyper", "hyper-util", "mongodb", @@ -12475,7 +12167,7 @@ dependencies = [ "temps-query-redis", "temps-query-s3", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-postgres", "tokio-test", @@ -12487,12 +12179,12 @@ dependencies = [ [[package]] name = "temps-proxy" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "argon2", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", @@ -12501,7 +12193,7 @@ dependencies = [ "flate2", "futures", "hex", - "hmac", + "hmac 0.12.1", "htmd", "http-body-util", "hyper", @@ -12527,7 +12219,7 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx", "temp-dir", "temps-analytics", @@ -12543,7 +12235,7 @@ dependencies = [ "temps-presets", "temps-routes", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tower 0.4.13", @@ -12556,7 +12248,7 @@ dependencies = [ [[package]] name = "temps-pty-agent" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "bytes", "clap", @@ -12565,7 +12257,7 @@ dependencies = [ "nix 0.29.0", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -12574,7 +12266,7 @@ dependencies = [ [[package]] name = "temps-query" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "bytes", @@ -12601,14 +12293,14 @@ dependencies = [ "serde", "serde_json", "temps-query", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "temps-query-postgres" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "async-trait", "chrono", @@ -12634,7 +12326,7 @@ dependencies = [ "serde", "serde_json", "temps-query", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -12656,7 +12348,7 @@ dependencies = [ "serde", "serde_json", "temps-query", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -12664,12 +12356,12 @@ dependencies = [ [[package]] name = "temps-queue" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "log", "serde", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "utoipa", @@ -12677,31 +12369,31 @@ dependencies = [ [[package]] name = "temps-revenue" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "bytes", "chrono", "csv", "hex", - "hmac", - "http 1.3.1", + "hmac 0.12.1", + "http 1.4.0", "rand 0.8.6", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "subtle", "temps-auth", "temps-core", "temps-database", "temps-entities", "temps-migrations", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -12711,19 +12403,19 @@ dependencies = [ [[package]] name = "temps-routes" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "chrono", - "http 1.3.1", + "http 1.4.0", "parking_lot", "sea-orm", "sea-orm-migration", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx", "subtle", "temps-core", @@ -12735,18 +12427,18 @@ dependencies = [ [[package]] name = "temps-sandbox" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "argon2", "async-stream", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "chrono", "flate2", "futures", "hex", - "http 1.3.1", + "http 1.4.0", "rand 0.8.6", "sea-orm", "serde", @@ -12758,10 +12450,10 @@ dependencies = [ "temps-core", "temps-entities", "temps-git", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "utoipa", @@ -12769,11 +12461,11 @@ dependencies = [ [[package]] name = "temps-screenshots" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.21.7", "headless_chrome", "mockito", @@ -12785,7 +12477,7 @@ dependencies = [ "temps-config", "temps-core", "temps-database", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -12795,9 +12487,9 @@ dependencies = [ [[package]] name = "temps-static-files" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "temps-config", "temps-core", "tokio", @@ -12807,16 +12499,16 @@ dependencies = [ [[package]] name = "temps-status-page" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "axum-macros", "chrono", "futures", "hex", - "http 1.3.1", + "http 1.4.0", "reqwest", "sea-orm", "serde", @@ -12829,7 +12521,7 @@ dependencies = [ "temps-entities", "temps-migrations", "temps-projects", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -12840,17 +12532,17 @@ dependencies = [ [[package]] name = "temps-vulnerability-scanner" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "bollard", "bytes", "chrono", "flate2", "futures", - "http 1.3.1", + "http 1.4.0", "sea-orm", "serde", "serde_json", @@ -12862,7 +12554,7 @@ dependencies = [ "temps-entities", "temps-projects", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", "tracing", @@ -12872,29 +12564,29 @@ dependencies = [ [[package]] name = "temps-webhooks" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "anyhow", "async-trait", - "axum 0.8.6", + "axum 0.8.9", "chrono", "hex", - "hmac", + "hmac 0.12.1", "reqwest", "sea-orm", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "temps-auth", "temps-core", "temps-database", "temps-entities", "temps-queue", "testcontainers", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-test", - "tower 0.5.2", + "tower 0.5.3", "tracing", "url", "utoipa", @@ -12903,7 +12595,7 @@ dependencies = [ [[package]] name = "temps-wireguard" -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" dependencies = [ "base64 0.22.1", "defguard_wireguard_rs", @@ -12911,7 +12603,7 @@ dependencies = [ "serde", "serde_json", "temps-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "x25519-dalek", @@ -12928,6 +12620,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -12945,9 +12647,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "testcontainers" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd36b06a2a6c0c3c81a83be1ab05fe86460d054d4d51bf513bc56b3e15bdc22" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" dependencies = [ "astral-tokio-tar", "async-trait", @@ -12958,7 +12660,7 @@ dependencies = [ "etcetera 0.11.0", "ferroid", "futures", - "http 1.3.1", + "http 1.4.0", "itertools 0.14.0", "log", "memchr", @@ -12966,8 +12668,8 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "serde_with 3.15.1", - "thiserror 2.0.17", + "serde_with 3.20.0", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -12994,11 +12696,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -13009,18 +12711,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -13044,9 +12746,9 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", @@ -13113,9 +12815,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -13123,9 +12825,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -13154,7 +12856,7 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -13167,7 +12869,7 @@ dependencies = [ "clap", "derive_builder 0.12.0", "esaxx-rs", - "getrandom 0.2.16", + "getrandom 0.2.17", "indicatif", "itertools 0.12.1", "lazy_static", @@ -13192,30 +12894,30 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.1.0", + "mio 1.2.0", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -13241,9 +12943,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b40d66d9b2cfe04b628173409368e58247e8eddbbd3b0e6c6ba1d09f20f6c9e" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" dependencies = [ "async-trait", "byteorder", @@ -13258,11 +12960,11 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.4", - "socket2 0.6.1", + "rand 0.10.1", + "socket2 0.6.3", "tokio", "tokio-util", - "whoami", + "whoami 2.1.2", ] [[package]] @@ -13290,11 +12992,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -13304,12 +13018,10 @@ dependencies = [ [[package]] name = "tokio-test" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" dependencies = [ - "async-stream", - "bytes", "futures-core", "tokio", "tokio-stream", @@ -13343,11 +13055,23 @@ dependencies = [ "tungstenite 0.28.0", ] +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.29.0", +] + [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -13373,11 +13097,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -13396,39 +13135,48 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.12.0", - "toml_datetime 0.7.5+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.13", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.13", + "winnow 1.0.3", ] [[package]] @@ -13437,6 +13185,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tonic" version = "0.13.1" @@ -13446,7 +13200,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -13457,7 +13211,7 @@ dependencies = [ "prost 0.13.5", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -13465,16 +13219,16 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "bytes", - "h2 0.4.12", - "http 1.3.1", + "h2 0.4.14", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "hyper", @@ -13482,11 +13236,11 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.1", + "socket2 0.6.3", "sync_wrapper", "tokio", "tokio-stream", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -13494,26 +13248,26 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.2" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.1", - "tonic 0.14.2", + "prost 0.14.3", + "tonic 0.14.6", ] [[package]] name = "totp-rs" -version = "5.7.0" +version = "5.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72" +checksum = "a2b36a9dd327e9f401320a2cb4572cc76ff43742bcfc3291f871691050f140ba" dependencies = [ "base32", "constant_time_eq", - "hmac", + "hmac 0.12.1", "sha1 0.10.6", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -13533,13 +13287,13 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.12.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -13556,10 +13310,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "cookie 0.18.1", "futures-util", - "http 1.3.1", + "http 1.4.0", "parking_lot", "pin-project-lite", "tower-layer", @@ -13568,30 +13322,30 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body 1.0.1", "http-body-util", "http-range-header", "httpdate", - "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -13608,9 +13362,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -13620,20 +13374,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -13662,9 +13416,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -13715,7 +13469,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.8.6", @@ -13734,33 +13488,49 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.9.4", "sha1 0.10.6", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.4", + "sha1 0.10.6", + "thiserror 2.0.18", +] + [[package]] name = "typed-builder" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -13777,9 +13547,9 @@ checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "typetag" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +checksum = "c5a897b12c6c1151ad0b138b8db50252dc301f93bc3b027db05eec82aeed298c" dependencies = [ "erased-serde", "inventory", @@ -13790,13 +13560,13 @@ dependencies = [ [[package]] name = "typetag-impl" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +checksum = "cf808357c6ed7e13ba0f3277ec8d8f21b2d501274895104263985330c726c1c5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -13807,9 +13577,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -13825,15 +13595,15 @@ checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -13849,15 +13619,15 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -13885,9 +13655,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "uniffi" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c6dec3fc6645f71a16a3fa9ff57991028153bd194ca97f4b55e610c73ce66a" +checksum = "dc5f2297ee5b893405bed1a6929faec4713a061df158ecf5198089f23910d470" dependencies = [ "anyhow", "camino", @@ -13902,9 +13672,9 @@ dependencies = [ [[package]] name = "uniffi_bindgen" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed0150801958d4825da56a41c71f000a457ac3a4613fa9647df78ac4b6b6881" +checksum = "8bc0c60a9607e7ab77a2ad47ec5530178015014839db25af7512447d2238016c" dependencies = [ "anyhow", "askama", @@ -13914,12 +13684,12 @@ dependencies = [ "glob", "goblin", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "once_cell", "serde", "tempfile", "textwrap", - "toml 0.8.23", + "toml 0.9.12+spec-1.1.0", "uniffi_internal_macros", "uniffi_meta", "uniffi_pipeline", @@ -13928,9 +13698,9 @@ dependencies = [ [[package]] name = "uniffi_build" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b78fd9271a4c2e85bd2c266c5a9ede1fac676eb39fd77f636c27eaf67426fd5f" +checksum = "4c39413c43b955e4aa8a4e2b34bbd1b6b5ff6bd85532b52f9eb92fbe88c14458" dependencies = [ "anyhow", "camino", @@ -13939,9 +13709,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0ef62e69762fbb9386dcb6c87cd3dd05d525fa8a3a579a290892e60ddbda47e" +checksum = "77baf5d539fe2e1ad6805e942dbc5dbdeb2b83eb5f2b3a6535d422ca4b02a12f" dependencies = [ "anyhow", "bytes", @@ -13951,22 +13721,22 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f51ebca0d9a4b2aa6c644d5ede45c56f73906b96403c08a1985e75ccb64a01" +checksum = "b4b42137524f4be6400fcaca9d02c1d4ecb6ad917e4013c0b93235526d8396e5" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "uniffi_macros" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9d12529f1223d014fd501e5f29ca0884d15d6ed5ddddd9f506e55350327dc3" +checksum = "d9273ec45330d8fe9a3701b7b983cea7a4e218503359831967cb95d26b873561" dependencies = [ "camino", "fs-err", @@ -13974,16 +13744,16 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.108", - "toml 0.8.23", + "syn 2.0.117", + "toml 0.9.12+spec-1.1.0", "uniffi_meta", ] [[package]] name = "uniffi_meta" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df6d413db2827c68588f8149d30d49b71d540d46539e435b23a7f7dbd4d4f86" +checksum = "431d2f443e7828a6c29d188de98b6771a6491ee98bba2d4372643bf93f988a18" dependencies = [ "anyhow", "siphasher", @@ -13993,22 +13763,22 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a806dddc8208f22efd7e95a5cdf88ed43d0f3271e8f63b47e757a8bbdb43b63a" +checksum = "761ef74f6175e15603d0424cc5f98854c5baccfe7bf4ccb08e5816f9ab8af689" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "tempfile", "uniffi_internal_macros", ] [[package]] name = "uniffi_udl" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d1a7339539bf6f6fa3e9b534dece13f778bda2d54b1a6d4e40b4d6090ac26e7" +checksum = "68773ec0e1c067b6505a73bbf6a5782f31a7f9209333a0df97b87565c46bf370" dependencies = [ "anyhow", "textwrap", @@ -14022,7 +13792,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.6", + "crypto-common 0.1.7", "subtle", ] @@ -14040,45 +13810,45 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.2" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", "flate2", "log", "percent-encoding", "rustls", - "rustls-pemfile", "rustls-pki-types", "socks", "ureq-proto", - "utf-8", - "webpki-roots 1.0.3", + "utf8-zero", + "webpki-roots 1.0.7", ] [[package]] name = "ureq-proto" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64 0.22.1", - "http 1.3.1", + "http 1.4.0", "httparse", "log", ] [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -14093,6 +13863,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -14107,11 +13883,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.14.0", "serde", "serde_json", "utoipa-gen", @@ -14119,14 +13895,14 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -14135,7 +13911,7 @@ version = "9.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" dependencies = [ - "axum 0.8.6", + "axum 0.8.9", "base64 0.22.1", "mime_guess", "regex", @@ -14155,30 +13931,18 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "rand 0.9.4", - "serde", + "rand 0.10.1", + "serde_core", "sha1_smol", - "uuid-macro-internal", "wasm-bindgen", ] -[[package]] -name = "uuid-macro-internal" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "v_frame" version = "0.3.9" @@ -14217,7 +13981,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -14232,12 +13996,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -14284,13 +14042,22 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen 0.57.1", ] [[package]] @@ -14309,50 +14076,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] -name = "wasm-bindgen" -version = "0.2.104" +name = "wasite" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" +name = "wasm-bindgen" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.108", + "cfg-if", + "once_cell", + "rustversion", + "serde", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -14360,50 +14120,65 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.54" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" dependencies = [ + "async-trait", + "cast", "js-sys", + "libm", "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.54" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -14421,7 +14196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.12.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -14445,17 +14220,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hashbrown 0.15.5", - "indexmap 2.12.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -14474,46 +14249,14 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf 0.11.3", - "phf_codegen 0.11.3", - "string_cache 0.8.9", - "string_cache_codegen 0.5.4", -] - -[[package]] -name = "web_atoms" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ "phf 0.13.1", "phf_codegen 0.13.1", - "string_cache 0.9.0", - "string_cache_codegen 0.6.1", -] - -[[package]] -name = "webdriver" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144ab979b12d36d65065635e646549925de229954de2eb3b47459b432a42db71" -dependencies = [ - "base64 0.21.7", - "bytes", - "cookie 0.16.2", - "http 0.2.12", - "log", - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.69", - "time", - "unicode-segmentation", - "url", + "string_cache", + "string_cache_codegen", ] [[package]] @@ -14540,14 +14283,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -14563,19 +14306,17 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" -version = "8.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix 1.1.2", - "winsafe", + "libc", ] [[package]] @@ -14585,7 +14326,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ "libredox", - "wasite", + "wasite 0.1.0", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite 1.0.2", "web-sys", ] @@ -14689,9 +14442,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement 0.60.2", "windows-interface 0.59.3", - "windows-link 0.2.1", + "windows-link", "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-strings", ] [[package]] @@ -14701,7 +14454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core 0.62.2", - "windows-link 0.2.1", + "windows-link", "windows-threading", ] @@ -14713,7 +14466,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -14724,7 +14477,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -14735,7 +14488,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -14746,15 +14499,9 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -14768,18 +14515,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core 0.62.2", - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link", + "windows-result 0.4.1", + "windows-strings", ] [[package]] @@ -14791,31 +14538,13 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -14824,7 +14553,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -14869,7 +14598,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -14909,7 +14638,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -14926,7 +14655,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -15078,21 +14807,20 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", ] [[package]] @@ -15105,20 +14833,14 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wireguard-nt" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22b4dbcc6c93786cf22e420ef96e8976bfb92a455070282302b74de5848191f4" dependencies = [ - "bitflags 2.10.0", - "getrandom 0.2.16", + "bitflags 2.11.1", + "getrandom 0.2.17", "ipnet", "libloading", "log", @@ -15137,7 +14859,7 @@ dependencies = [ "base64 0.22.1", "deadpool", "futures", - "http 1.3.1", + "http 1.4.0", "http-body-util", "hyper", "hyper-util", @@ -15150,12 +14872,6 @@ dependencies = [ "url", ] -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -15165,6 +14881,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -15184,9 +14906,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.12.0", + "indexmap 2.14.0", "prettyplease", - "syn 2.0.108", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -15202,7 +14924,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -15214,8 +14936,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", - "indexmap 2.12.0", + "bitflags 2.11.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -15234,7 +14956,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.12.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -15256,9 +14978,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -15288,8 +15010,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", - "der 0.7.10", - "spki 0.7.3", + "der", + "spki", "tls_codec", ] @@ -15313,18 +15035,18 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs 0.7.1", + "asn1-rs 0.7.2", "data-encoding", "der-parser 10.0.0", "lazy_static", "nom 7.1.3", "oid-registry 0.8.1", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -15335,17 +15057,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] name = "xml5ever" -version = "0.35.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494" +checksum = "d3dc9559429edf0cd3f327cc0afd9d6b36fa8cec6d93107b7fbe64f806b5f2d9" dependencies = [ "log", - "markup5ever 0.35.0", + "markup5ever 0.38.0", ] [[package]] @@ -15363,6 +15085,12 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yaml-rust2" version = "0.8.1" @@ -15391,11 +15119,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -15403,54 +15130,54 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", "synstructure", ] @@ -15465,20 +15192,20 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -15487,9 +15214,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -15498,13 +15225,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.117", ] [[package]] @@ -15523,13 +15250,13 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", - "indexmap 2.12.0", + "hmac 0.12.1", + "indexmap 2.14.0", "lzma-rs", "memchr", - "pbkdf2 0.12.2", + "pbkdf2", "sha1 0.10.6", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "xz2", "zeroize", @@ -15546,7 +15273,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.12.0", + "indexmap 2.14.0", "memchr", "zopfli", ] @@ -15565,11 +15292,11 @@ dependencies = [ "deflate64", "flate2", "getrandom 0.3.4", - "hmac", - "indexmap 2.12.0", + "hmac 0.12.1", + "indexmap 2.14.0", "lzma-rust2", "memchr", - "pbkdf2 0.12.2", + "pbkdf2", "ppmd-rust", "sha1 0.10.6", "time", @@ -15580,15 +15307,21 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", @@ -15626,9 +15359,9 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-inflate" @@ -15641,9 +15374,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index ed32cffb..5e9cf0e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ members = [ "crates/temps-notifications", "crates/temps-monitoring", "crates/temps-network", - "crates/temps-mcp", "crates/temps-analytics", "crates/temps-audit", "crates/temps-config", @@ -84,7 +83,7 @@ members = [ ] [workspace.package] -version = "0.1.0-beta.15" +version = "0.1.0-beta.19" edition = "2021" license = "Apache-2.0" authors = ["Temps Contributors"] diff --git a/apps/temps-cli/package.json b/apps/temps-cli/package.json index dce25dc9..f394264e 100644 --- a/apps/temps-cli/package.json +++ b/apps/temps-cli/package.json @@ -1,6 +1,6 @@ { "name": "@temps-sdk/cli", - "version": "0.1.21", + "version": "0.1.23", "description": "CLI for Temps deployment platform", "type": "module", "bin": { diff --git a/apps/temps-cli/src/commands/auth/index.ts b/apps/temps-cli/src/commands/auth/index.ts index d726ac9d..a0741a75 100644 --- a/apps/temps-cli/src/commands/auth/index.ts +++ b/apps/temps-cli/src/commands/auth/index.ts @@ -9,6 +9,7 @@ export function registerAuthCommands(program: Command): void { .description('Authenticate with a Temps server. Opens the browser for interactive logins; use --api-key for headless / CI.') .option('-k, --api-key ', 'Use a pre-minted API key (Settings → API Keys) instead of opening the browser. Required for headless / CI.') .option('--context ', 'Save the credentials under this context name (defaults to URL host)') + .option('--debug', 'Print every request/response (URL, status, headers, raw body) to stderr. Also enabled via TEMPS_DEBUG=1.') .action(async (url: string | undefined, opts: Record) => { // Forward the positional `url` as if it were `--url`. Commander // doesn't surface positional args via opts. diff --git a/apps/temps-cli/src/commands/auth/login.ts b/apps/temps-cli/src/commands/auth/login.ts index dcd8b4e3..c818e4d2 100644 --- a/apps/temps-cli/src/commands/auth/login.ts +++ b/apps/temps-cli/src/commands/auth/login.ts @@ -16,20 +16,109 @@ interface LoginOptions { context?: string /** Override the server URL for this login (otherwise uses config / active context). */ url?: string + /** Emit verbose request/response logging for diagnosing connection issues. */ + debug?: boolean } /** - * Strip the "/api" suffix that `normalizeApiUrl` appends, since the - * `/auth/cli/device/*` endpoints sit at the server root, not under `/api`. - * Also tolerates the user passing the bare host with or without scheme. + * Whether verbose request/response logging is enabled for this invocation. + * Activated by `--debug` on the command or `TEMPS_DEBUG=1` in the environment. + */ +function debugEnabled(opts: { debug?: boolean } = {}): boolean { + if (opts.debug) return true + const env = process.env.TEMPS_DEBUG + return env === '1' || env === 'true' || env === 'yes' +} + +function debugLog(message: string, payload?: unknown): void { + if (payload === undefined) { + process.stderr.write(`[temps-cli debug] ${message}\n`) + return + } + let rendered: string + try { + rendered = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2) + } catch { + rendered = String(payload) + } + process.stderr.write(`[temps-cli debug] ${message} ${rendered}\n`) +} + +/** + * Wraps `fetch` so debug mode can see exactly what was sent and what came back. + * We read the raw body as text first, log it, and then re-parse as JSON so the + * caller still gets a parsed object (or a typed error pointing at the raw body). + */ +async function debugFetch( + url: string, + init: RequestInit, + debug: boolean, +): Promise<{ res: Response; rawBody: string; json: unknown }> { + if (debug) { + debugLog(`-> ${init.method ?? 'GET'} ${url}`) + if (init.body) debugLog(' request body:', init.body) + } + let res: Response + try { + res = await fetch(url, init) + } catch (err) { + const reason = err instanceof Error ? err.message : String(err) + if (debug) debugLog(` fetch failed: ${reason}`) + throw new AuthenticationError( + `Unable to connect to ${url}: ${reason}. Is the server reachable from this machine?`, + ) + } + const rawBody = await res.text() + if (debug) { + debugLog(`<- ${res.status} ${res.statusText} (${url})`) + const headers: Record = {} + res.headers.forEach((value, key) => { + headers[key] = value + }) + debugLog(' response headers:', headers) + const preview = rawBody.length > 2000 ? `${rawBody.slice(0, 2000)}…[truncated]` : rawBody + debugLog(' response body:', preview || '(empty)') + } + let json: unknown = null + if (rawBody.length > 0) { + try { + json = JSON.parse(rawBody) + } catch { + json = null + } + } + return { res, rawBody, json } +} + +/** + * The device-auth endpoints are served by the auth plugin, which is mounted + * under `/api` by the core router (see `temps-core/src/plugin.rs:760`). So + * the real URLs are `/api/auth/cli/device/start` and `…/poll`. + * + * Users may pass: + * - bare host: `https://app.temps.kfs.es` -> add `/api` + * - with prefix: `https://app.temps.kfs.es/api` -> keep as-is + * - with trailing slash: `https://app.temps.kfs.es/` -> normalize then add `/api` + * + * Returns the `/api`-suffixed base, with no trailing slash. */ function serverBaseUrl(rawApiUrl: string): string { - return rawApiUrl.replace(/\/+$/, '').replace(/\/api$/, '') + const trimmed = rawApiUrl.replace(/\/+$/, '') + return /\/api$/.test(trimmed) ? trimmed : `${trimmed}/api` } export async function login(options: LoginOptions): Promise { newline() + const debug = debugEnabled(options) + if (debug) { + debugLog('login invoked with options:', { + url: options.url, + context: options.context, + hasApiKey: !!options.apiKey, + }) + } + // If the user is already logged in AND isn't pointing at a new server / // context, refuse — they can `temps logout` to switch. When a new --url // or --context is supplied we treat the call as "add another context" @@ -50,7 +139,7 @@ export async function login(options: LoginOptions): Promise { // Interactive path: browser-based device-authorization. Credentials are // always entered in the web app — the CLI never prompts for a password. - await loginWithDevice({ url: options.url, context: options.context }) + await loginWithDevice({ url: options.url, context: options.context, debug }) } /** @@ -64,13 +153,23 @@ export async function login(options: LoginOptions): Promise { * - keeping the credential surface area in the browser, not the shell. */ export async function loginWithDevice( - opts: { url?: string; context?: string } = {}, + opts: { url?: string; context?: string; debug?: boolean } = {}, ): Promise { - const baseUrl = opts.url + const debug = debugEnabled(opts) + // `apiBaseUrl` is the `/api`-prefixed URL the auth plugin actually lives at + // (since `temps-core` nests plugin routes under `/api`). `webBaseUrl` is the + // frontend root, used to resolve `/cli-login` URLs the user opens in a browser. + const apiBaseUrl = opts.url ? serverBaseUrl(opts.url) : serverBaseUrl(config.get('apiUrl')) + const webBaseUrl = apiBaseUrl.replace(/\/api$/, '') if (opts.url) { - config.set('apiUrl', baseUrl) + config.set('apiUrl', apiBaseUrl) + } + if (debug) { + debugLog(`resolved apiBaseUrl: ${apiBaseUrl}`) + debugLog(`resolved webBaseUrl: ${webBaseUrl}`) + debugLog(`raw url arg: ${opts.url ?? '(none, using config apiUrl)'}`) } const deviceName = (() => { @@ -82,28 +181,44 @@ export async function loginWithDevice( })() // 1. Start a device session. + const startUrl = `${apiBaseUrl}/auth/cli/device/start` const start = await withSpinner( 'Requesting device authorization...', async () => { - const res = await fetch(`${baseUrl}/auth/cli/device/start`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ client_name: deviceName }), - }) + const { res, rawBody, json } = await debugFetch( + startUrl, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ client_name: deviceName }), + }, + debug, + ) if (!res.ok) { - const problem = await safeJson<{ title?: string; detail?: string }>(res) + const problem = (json as { title?: string; detail?: string } | null) ?? null throw new AuthenticationError( problem?.detail || problem?.title || - `Device authorization start failed (status ${res.status})`, + `Device authorization start failed at ${startUrl} (status ${res.status}). ` + + `Response body: ${rawBody.slice(0, 200) || '(empty)'}`, + ) + } + if (json === null) { + throw new AuthenticationError( + `Server at ${startUrl} returned a non-JSON response (status ${res.status}, ` + + `content-type ${res.headers.get('content-type') ?? 'unknown'}). ` + + `First 200 chars: ${rawBody.slice(0, 200) || '(empty)'}. ` + + `Re-run with --debug for the full response.`, ) } - return (await res.json()) as DeviceStartResponse + return json as DeviceStartResponse }, { successText: 'Device authorization ready' }, ) - const fullUrl = absoluteVerificationUri(baseUrl, start) + // `verification_uri{,_complete}` are frontend paths (e.g. `/cli-login/CODE`), + // so they must resolve against the web root, NOT the `/api` base. + const fullUrl = absoluteVerificationUri(webBaseUrl, start) // 2. Tell the user what to do. newline() @@ -114,7 +229,7 @@ export async function loginWithDevice( ``, `If your browser doesn't open automatically, paste the code:`, ` ${colors.bold(start.user_code)}`, - `at ${colors.muted(`${baseUrl}/cli-login`)}`, + `at ${colors.muted(`${webBaseUrl}/cli-login`)}`, ].join('\n'), `${icons.sparkles} Authorize the Temps CLI`, ) @@ -132,23 +247,35 @@ export async function loginWithDevice( `Waiting for browser approval (code ${colors.bold(start.user_code)})...`, async () => { let pollDelay = intervalMs + const pollUrl = `${apiBaseUrl}/auth/cli/device/poll` // Loop until the server reaches a terminal state or we time out. while (Date.now() < deadline) { await sleep(pollDelay) - const res = await fetch(`${baseUrl}/auth/cli/device/poll`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ device_code: start.device_code }), - }) + const { res, rawBody, json } = await debugFetch( + pollUrl, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ device_code: start.device_code }), + }, + debug, + ) if (!res.ok) { - const problem = await safeJson<{ title?: string; detail?: string }>(res) + const problem = (json as { title?: string; detail?: string } | null) ?? null throw new AuthenticationError( problem?.detail || problem?.title || - `Polling failed (status ${res.status})`, + `Polling failed at ${pollUrl} (status ${res.status}). ` + + `Response body: ${rawBody.slice(0, 200) || '(empty)'}`, + ) + } + if (json === null) { + throw new AuthenticationError( + `Server at ${pollUrl} returned a non-JSON response (status ${res.status}). ` + + `First 200 chars: ${rawBody.slice(0, 200) || '(empty)'}`, ) } - const body = (await res.json()) as DevicePollResponse + const body = json as DevicePollResponse switch (body.status) { case 'authorization_pending': pollDelay = intervalMs @@ -176,24 +303,25 @@ export async function loginWithDevice( { successText: 'Browser approval received' }, ) - // 5. Persist credentials. - const contextName = opts.context ?? defaultContextName(baseUrl) + // 5. Persist credentials. We store the `/api`-prefixed URL since that's + // what the rest of the CLI (and `normalizeApiUrl`) expects as `apiUrl`. + const contextName = opts.context ?? defaultContextName(apiBaseUrl) await upsertContext({ name: contextName, - url: baseUrl, + url: apiBaseUrl, apiKey: success.api_key, email: success.email, keyPrefix: success.key_prefix, expiresAt: success.expires_at ?? undefined, }) - config.set('apiUrl', baseUrl) + config.set('apiUrl', apiBaseUrl) await credentials.setAll({ apiKey: success.api_key, userId: success.user_id, email: success.email, }) - displayWelcome(success.email, contextName, baseUrl, { + displayWelcome(success.email, contextName, apiBaseUrl, { role: success.role, key_prefix: success.key_prefix, expires_at: success.expires_at, @@ -277,14 +405,6 @@ async function tryOpenBrowser(url: string): Promise { } } -async function safeJson(res: Response): Promise { - try { - return (await res.json()) as T - } catch { - return null - } -} - function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/apps/temps-cli/src/commands/projects/create.ts b/apps/temps-cli/src/commands/projects/create.ts index 6a242844..a53418b6 100644 --- a/apps/temps-cli/src/commands/projects/create.ts +++ b/apps/temps-cli/src/commands/projects/create.ts @@ -1,5 +1,11 @@ import { requireAuth, config } from '../../config/store.js' -import { promptText, promptConfirm, promptSelect, type SelectOption } from '../../ui/prompts.js' +import { + promptText, + promptConfirm, + promptSelect, + promptNumber, + type SelectOption, +} from '../../ui/prompts.js' import { withSpinner } from '../../ui/spinner.js' import { success, @@ -14,7 +20,7 @@ import { } from '../../ui/output.js' import { setupClient, client, getErrorMessage } from '../../lib/api-client.js' import { createProject } from '../../api/sdk.gen.js' -import type { RepositoryResponse } from '../../api/types.gen.js' +import type { RepositoryResponse, SourceType } from '../../api/types.gen.js' import { readEnvFile, findEnvFiles } from '../../lib/env-file.js' // Shared utilities (extracted to avoid duplication with setup wizard) @@ -36,8 +42,36 @@ interface CreateOptions { connection?: string repo?: string yes?: boolean + // Manual (non-git) deployment mode + manual?: boolean + sourceType?: string + image?: string + port?: string } +// Manual deployment methods (non-git). Mirrors the web ManualProjectConfigurator. +const MANUAL_SOURCE_TYPES: { + value: Exclude + name: string + description: string +}[] = [ + { + value: 'manual', + name: 'Flexible (Recommended)', + description: 'Deploy via Docker images, static files, or Git - switch anytime', + }, + { + value: 'docker_image', + name: 'Docker Image Only', + description: 'Locked to Docker image deployments only', + }, + { + value: 'static_files', + name: 'Static Files Only', + description: 'Locked to static file deployments only', + }, +] + export async function create(options: CreateOptions): Promise { await requireAuth() await setupClient() @@ -49,6 +83,56 @@ export async function create(options: CreateOptions): Promise { console.log(colors.muted('─'.repeat(40))) newline() + // Determine deployment method. Manual mode skips git connection/repo/branch/preset. + const manualRequested = + options.manual === true || + options.sourceType !== undefined || + options.image !== undefined || + options.port !== undefined + + let manualSourceType: Exclude | undefined + + if (manualRequested) { + if (options.sourceType) { + const match = MANUAL_SOURCE_TYPES.find((t) => t.value === options.sourceType) + if (!match) { + error( + `Invalid --source-type "${options.sourceType}". Use one of: ${MANUAL_SOURCE_TYPES.map((t) => t.value).join(', ')}` + ) + return + } + manualSourceType = match.value + } else { + // --manual / --image / --port with no explicit source type defaults to flexible + manualSourceType = 'manual' + } + } else if (!skipPrompts && !options.repo && !options.connection) { + // Interactive: let the user pick git vs a manual method upfront. + const choice = await promptSelect<'git' | Exclude>({ + message: 'How do you want to deploy this project?', + choices: [ + { + name: 'Git Repository', + value: 'git', + description: 'Connect a repo - builds and deploys on every push', + }, + ...MANUAL_SOURCE_TYPES.map((t) => ({ + name: t.name, + value: t.value, + description: t.description, + })), + ], + }) + if (choice !== 'git') { + manualSourceType = choice + } + } + + if (manualSourceType) { + await createManualProject(options, manualSourceType, skipPrompts) + return + } + try { // Step 1: Select Git Connection let connection @@ -222,6 +306,160 @@ export async function create(options: CreateOptions): Promise { } } +/** + * Manual (non-git) project creation flow. + * + * Mirrors the web ManualProjectConfigurator: pick a deployment method + * (flexible / docker_image / static_files), optionally a Docker image and + * port, then storage services and environment variables. + */ +async function createManualProject( + options: CreateOptions, + sourceType: Exclude, + skipPrompts: boolean +): Promise { + const methodMeta = MANUAL_SOURCE_TYPES.find((t) => t.value === sourceType)! + info(`Deployment method: ${methodMeta.name}`) + + try { + // Step 1: Project name + let projectName: string + if (options.name) { + projectName = options.name + info(`Using project name: ${projectName}`) + } else if (skipPrompts) { + error('Project name is required. Pass --name when using --yes.') + return + } else { + newline() + projectName = await promptText({ + message: 'Project name', + required: true, + validate: (v) => (v.length >= 2 ? true : 'Name must be at least 2 characters'), + }) + } + + // Step 2: Docker image (only for flexible/docker_image, always optional) + let dockerImage: string | undefined + if (sourceType === 'manual' || sourceType === 'docker_image') { + if (options.image) { + dockerImage = options.image + info(`Using Docker image: ${dockerImage}`) + } else if (!skipPrompts) { + const entered = await promptText({ + message: 'Docker image (optional, e.g. nginx:latest or ghcr.io/org/image:tag)', + required: false, + }) + dockerImage = entered.trim() || undefined + } + } + + // Step 3: Application port + let port: number + if (options.port) { + const parsed = parseInt(options.port, 10) + if (isNaN(parsed) || parsed < 1 || parsed > 65535) { + error(`Invalid --port "${options.port}". Must be a number between 1 and 65535.`) + return + } + port = parsed + info(`Using port: ${port}`) + } else if (skipPrompts) { + port = 3000 + } else { + port = await promptNumber( + sourceType === 'docker_image' ? 'Container port' : 'Application port', + { default: 3000, min: 1, max: 65535 } + ) + } + + // Step 4: Storage services (skip with --yes) + const serviceIds = skipPrompts ? [] : await selectStorageServices() + + // Step 5: Environment variables (skip with --yes) + const envVars = skipPrompts ? [] : await configureEnvironmentVariables() + + // Step 6: Create the project. project_type mirrors the web configurator. + const projectType = sourceType === 'static_files' ? 'static' : 'docker' + + const project = await withSpinner('Creating project...', async () => { + const { data, error: apiError } = await createProject({ + client, + body: { + name: projectName, + preset: 'dockerfile', + directory: './', + main_branch: 'main', + source_type: sourceType, + project_type: projectType, + automatic_deploy: false, + exposed_port: port, + storage_service_ids: serviceIds, + environment_variables: envVars.length > 0 ? envVars : undefined, + }, + }) + + if (apiError || !data) { + throw new Error(getErrorMessage(apiError) || 'Failed to create project') + } + + return data + }) + + // Display success + newline() + header(`${icons.check} Project Created Successfully`) + newline() + + keyValue('ID', project.id) + keyValue('Name', project.name) + keyValue('Slug', project.slug) + keyValue('Deployment Method', methodMeta.name) + if (dockerImage) { + keyValue('Docker Image', `${dockerImage} ${colors.muted('(deploy with the command below)')}`) + } + keyValue('Port', port) + if (serviceIds.length > 0) { + keyValue('Services', `${serviceIds.length} linked`) + } + if (envVars.length > 0) { + keyValue('Environment Variables', `${envVars.length} configured`) + } + + newline() + + // Ask if user wants to set as default (auto-set with --yes) + if (skipPrompts) { + config.set('defaultProject', project.slug) + success(`Default project set to "${project.slug}"`) + } else { + const setDefault = await promptConfirm({ + message: 'Set as default project?', + default: true, + }) + + if (setDefault) { + config.set('defaultProject', project.slug) + success(`Default project set to "${project.slug}"`) + } + } + + newline() + info(`View your project: temps projects show ${project.slug}`) + if (sourceType === 'static_files') { + info(`Deploy static files: temps deploy:static -p ${project.slug} --path `) + } else { + const imageHint = dockerImage ?? '' + info(`Deploy a Docker image: temps deploy:image -p ${project.slug} --image ${imageHint}`) + if (sourceType === 'manual') { + info(`Or deploy static files: temps deploy:static -p ${project.slug} --path `) + } + } + } catch (err) { + error(getErrorMessage(err)) + } +} + /** * Step 5: Configure Project Name */ diff --git a/apps/temps-cli/src/commands/projects/index.ts b/apps/temps-cli/src/commands/projects/index.ts index 6f77f226..fa7a8a09 100644 --- a/apps/temps-cli/src/commands/projects/index.ts +++ b/apps/temps-cli/src/commands/projects/index.ts @@ -24,7 +24,7 @@ export function registerProjectsCommands(program: Command): void { projects .command('create') .alias('new') - .description('Create a new project') + .description('Create a new project (git-based or manual deployment)') .option('-n, --name ', 'Project name') .option('-d, --description ', 'Project description') .option('--repo ', 'Repository in owner/name format') @@ -32,6 +32,13 @@ export function registerProjectsCommands(program: Command): void { .option('--directory ', 'Root directory (relative to repo)') .option('--preset ', 'Build preset (e.g., nextjs, nodejs, static, docker)') .option('--connection ', 'Git connection ID') + .option('--manual', 'Create a manual (non-git) project - deploy via Docker image or static files') + .option( + '--source-type ', + 'Manual deployment method: manual (flexible), docker_image, or static_files' + ) + .option('--image ', 'Docker image for the first deployment (manual mode)') + .option('--port ', 'Application/container port (manual mode, default: 3000)') .option('-y, --yes', 'Skip optional prompts (services, env vars, set-default)') .action(create) diff --git a/crates/temps-ai-gateway/src/handlers/usage.rs b/crates/temps-ai-gateway/src/handlers/usage.rs index e18c88d1..3dc8b04f 100644 --- a/crates/temps-ai-gateway/src/handlers/usage.rs +++ b/crates/temps-ai-gateway/src/handlers/usage.rs @@ -16,7 +16,7 @@ use crate::error::AiGatewayError; use crate::handlers::types::AiGatewayAppState; use crate::services::usage_service::{ ConversationSummary, ModelUsage, ProviderUsage, TimeseriesBucket, UsageFilter, UsageLogEntry, - UsageSummary, + UsageLogPage, UsageSummary, }; // ============================================================================ @@ -45,6 +45,7 @@ use crate::services::usage_service::{ TimeseriesBucket, ModelUsage, UsageLogEntry, + UsageLogPage, ConversationSummary, UsageFilter, )), @@ -103,6 +104,7 @@ impl UsageQueryParams { tags: self.tags.clone(), model: self.model.clone(), provider: self.provider.clone(), + ..Default::default() } } } @@ -135,6 +137,7 @@ impl TimeseriesQueryParams { tags: self.tags.clone(), model: self.model.clone(), provider: self.provider.clone(), + ..Default::default() } } } @@ -163,10 +166,16 @@ impl TopModelsQueryParams { } } +/// Page size for the recent-requests endpoint. Defaults to 20, capped at 50. +pub const RECENT_DEFAULT_LIMIT: u64 = 20; +pub const RECENT_MAX_LIMIT: u64 = 50; + #[derive(Debug, Deserialize, ToSchema)] pub struct RecentQueryParams { - /// Max results (defaults to 50, max 100) + /// Page size (defaults to 20, max 50) pub limit: Option, + /// Number of results to skip for pagination (defaults to 0) + pub offset: Option, /// Filter by user ID pub user_id: Option, /// Filter by conversation ID @@ -177,6 +186,24 @@ pub struct RecentQueryParams { pub model: Option, /// Filter by provider name pub provider: Option, + /// Filter by HTTP status code (exact match) + pub status: Option, + /// Cost greater-than-or-equal, in microcents + pub cost_gte: Option, + /// Cost strictly greater-than, in microcents + pub cost_gt: Option, + /// Cost less-than-or-equal, in microcents + pub cost_lte: Option, + /// Cost strictly less-than, in microcents + pub cost_lt: Option, + /// Total tokens (input + output) greater-than-or-equal + pub tokens_gte: Option, + /// Total tokens (input + output) strictly greater-than + pub tokens_gt: Option, + /// Total tokens (input + output) less-than-or-equal + pub tokens_lte: Option, + /// Total tokens (input + output) strictly less-than + pub tokens_lt: Option, } impl RecentQueryParams { @@ -187,8 +214,24 @@ impl RecentQueryParams { tags: self.tags.clone(), model: self.model.clone(), provider: self.provider.clone(), + status: self.status, + cost_gte: self.cost_gte, + cost_gt: self.cost_gt, + cost_lte: self.cost_lte, + cost_lt: self.cost_lt, + tokens_gte: self.tokens_gte, + tokens_gt: self.tokens_gt, + tokens_lte: self.tokens_lte, + tokens_lt: self.tokens_lt, } } + + /// Resolve the effective page size: default 20, clamped to [1, 50]. + fn resolved_limit(&self) -> u64 { + self.limit + .unwrap_or(RECENT_DEFAULT_LIMIT) + .clamp(1, RECENT_MAX_LIMIT) + } } #[derive(Debug, Deserialize, ToSchema)] @@ -397,10 +440,22 @@ async fn get_usage_top_models( get, path = "/ai/usage/recent", params( - ("limit" = Option, Query, description = "Max results (defaults to 50, max 100)"), + ("limit" = Option, Query, description = "Page size (defaults to 20, max 50)"), + ("offset" = Option, Query, description = "Number of results to skip for pagination (defaults to 0)"), + ("provider" = Option, Query, description = "Filter by provider name"), + ("model" = Option, Query, description = "Filter by model name"), + ("status" = Option, Query, description = "Filter by HTTP status code (exact match)"), + ("cost_gte" = Option, Query, description = "Cost greater-than-or-equal, in microcents"), + ("cost_gt" = Option, Query, description = "Cost strictly greater-than, in microcents"), + ("cost_lte" = Option, Query, description = "Cost less-than-or-equal, in microcents"), + ("cost_lt" = Option, Query, description = "Cost strictly less-than, in microcents"), + ("tokens_gte" = Option, Query, description = "Total tokens greater-than-or-equal"), + ("tokens_gt" = Option, Query, description = "Total tokens strictly greater-than"), + ("tokens_lte" = Option, Query, description = "Total tokens less-than-or-equal"), + ("tokens_lt" = Option, Query, description = "Total tokens strictly less-than"), ), responses( - (status = 200, description = "Recent usage log entries", body = Vec), + (status = 200, description = "Page of recent usage log entries", body = UsageLogPage), (status = 401, description = "Unauthorized", body = ProblemDetails), (status = 403, description = "Insufficient permissions", body = ProblemDetails), ), @@ -413,14 +468,15 @@ async fn get_usage_recent( ) -> Result { permission_guard!(auth, AiGatewayRead); - let limit = std::cmp::min(params.limit.unwrap_or(50), 100); + let limit = params.resolved_limit(); + let offset = params.offset.unwrap_or(0); let filter = params.to_filter(); - let entries = app_state + let page = app_state .usage_service - .get_recent_filtered(limit, &filter) + .get_recent_filtered(limit, offset, &filter) .await?; - Ok(Json(entries)) + Ok(Json(page)) } #[utoipa::path( @@ -589,6 +645,41 @@ mod tests { let json = "{}"; let params: RecentQueryParams = serde_json::from_str(json).unwrap(); assert!(params.limit.is_none()); + assert!(params.offset.is_none()); + assert!(params.status.is_none()); + assert!(params.cost_gte.is_none()); + // Unspecified limit resolves to the default page size. + assert_eq!(params.resolved_limit(), RECENT_DEFAULT_LIMIT); + } + + #[test] + fn test_recent_query_params_limit_clamped_to_max() { + let params: RecentQueryParams = serde_json::from_str(r#"{"limit": 500}"#).unwrap(); + assert_eq!(params.resolved_limit(), RECENT_MAX_LIMIT); + } + + #[test] + fn test_recent_query_params_limit_floor_is_one() { + let params: RecentQueryParams = serde_json::from_str(r#"{"limit": 0}"#).unwrap(); + assert_eq!(params.resolved_limit(), 1); + } + + #[test] + fn test_recent_query_params_filters_map_to_usage_filter() { + let params: RecentQueryParams = serde_json::from_str( + r#"{"provider": "openai", "status": 429, "cost_gte": 100, "cost_lt": 5000, + "tokens_gte": 500, "tokens_lt": 10000}"#, + ) + .unwrap(); + let filter = params.to_filter(); + assert_eq!(filter.provider.as_deref(), Some("openai")); + assert_eq!(filter.status, Some(429)); + assert_eq!(filter.cost_gte, Some(100)); + assert_eq!(filter.cost_lt, Some(5000)); + assert_eq!(filter.cost_gt, None); + assert_eq!(filter.tokens_gte, Some(500)); + assert_eq!(filter.tokens_lt, Some(10000)); + assert_eq!(filter.tokens_gt, None); } #[test] diff --git a/crates/temps-ai-gateway/src/plugin.rs b/crates/temps-ai-gateway/src/plugin.rs index 27a750fb..67d51b42 100644 --- a/crates/temps-ai-gateway/src/plugin.rs +++ b/crates/temps-ai-gateway/src/plugin.rs @@ -70,25 +70,19 @@ impl TempsPlugin for AiGatewayPlugin { fn configure_routes(&self, context: &PluginContext) -> Option { let app_state = context.require_service::(); - // Admin: provider key management, usage analytics, pricing dashboard. + // All AI Gateway routes live on the authenticated surface. The + // OpenAI-compatible gateway endpoints use `RequireAuth`, which depends + // on the `AuthContext` injected by `auth_middleware` — that middleware + // only runs on this router, not the public one. let routes = handlers::configure_admin_routes() .merge(handlers::configure_usage_routes()) .merge(handlers::configure_pricing_routes()) + .merge(handlers::configure_gateway_routes()) .with_state(app_state); Some(PluginRoutes { router: routes }) } - fn configure_public_routes(&self, context: &PluginContext) -> Option { - let app_state = context.require_service::(); - - // Public: the OpenAI-compatible gateway endpoints. Auth is via API key - // tokens issued to deployed apps (handled inside the handlers). - let routes = handlers::configure_gateway_routes().with_state(app_state); - - Some(PluginRoutes { router: routes }) - } - fn openapi_schema(&self) -> Option { let mut schema = ::openapi(); let admin_schema = ::openapi(); diff --git a/crates/temps-ai-gateway/src/services/usage_service.rs b/crates/temps-ai-gateway/src/services/usage_service.rs index 7e140111..f56187f8 100644 --- a/crates/temps-ai-gateway/src/services/usage_service.rs +++ b/crates/temps-ai-gateway/src/services/usage_service.rs @@ -79,6 +79,15 @@ pub struct UsageLogEntry { pub trace_id: Option, } +/// A page of recent usage log entries plus the total count for pagination. +#[derive(Debug, Serialize, ToSchema)] +pub struct UsageLogPage { + /// The usage log entries for the requested page. + pub entries: Vec, + /// Total number of entries matching the filter (across all pages). + pub total: i64, +} + /// A conversation summary grouping related AI invocations. #[derive(Debug, Serialize, ToSchema)] pub struct ConversationSummary { @@ -104,6 +113,11 @@ pub struct AiRequestContext { } /// Filters for querying AI usage data. +/// +/// Cost bounds are expressed in microcents (the unit stored in +/// `estimated_cost_microcents`). At most one of `gte`/`gt` and one of +/// `lte`/`lt` is meaningful per query; if both are set the stricter wins +/// naturally because they are ANDead together. #[derive(Debug, Clone, Default, Deserialize, ToSchema)] pub struct UsageFilter { pub user_id: Option, @@ -112,6 +126,24 @@ pub struct UsageFilter { pub tags: Option, pub model: Option, pub provider: Option, + /// Filter by HTTP status code (exact match). + pub status: Option, + /// Cost greater-than-or-equal, in microcents. + pub cost_gte: Option, + /// Cost strictly greater-than, in microcents. + pub cost_gt: Option, + /// Cost less-than-or-equal, in microcents. + pub cost_lte: Option, + /// Cost strictly less-than, in microcents. + pub cost_lt: Option, + /// Total tokens (input + output) greater-than-or-equal. + pub tokens_gte: Option, + /// Total tokens (input + output) strictly greater-than. + pub tokens_gt: Option, + /// Total tokens (input + output) less-than-or-equal. + pub tokens_lte: Option, + /// Total tokens (input + output) strictly less-than. + pub tokens_lt: Option, } // ============================================================================ @@ -180,6 +212,11 @@ struct UsageLogRow { trace_id: Option, } +#[derive(Debug, FromQueryResult)] +struct CountRow { + count: Option, +} + #[derive(Debug, FromQueryResult)] struct ConversationSummaryRow { conversation_id: Option, @@ -539,22 +576,46 @@ impl UsageService { /// Get recent usage log entries. pub async fn get_recent(&self, limit: u64) -> Result, AiGatewayError> { - self.get_recent_filtered(limit, &UsageFilter::default()) - .await + Ok(self + .get_recent_filtered(limit, 0, &UsageFilter::default()) + .await? + .entries) } - /// Get recent usage log entries with filters. + /// Get a page of recent usage log entries with filters, plus the total count. pub async fn get_recent_filtered( &self, limit: u64, + offset: u64, filter: &UsageFilter, - ) -> Result, AiGatewayError> { + ) -> Result { // Use a wide time range for "recent" queries let to = Utc::now(); let from = to - chrono::Duration::days(365); - let (where_clause, mut values) = self.build_filter_clause(from, to, filter); - let next_param = values.len() + 1; + + // Total count for pagination -- the filter clause and its bound values are + // identical to the page query, so build them once and reuse for both. + let (where_clause, base_values) = self.build_filter_clause(from, to, filter); + + let count_sql = format!( + "SELECT COUNT(*) as count FROM ai_usage_logs WHERE {}", + where_clause + ); + let total = CountRow::find_by_statement(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + &count_sql, + base_values.clone(), + )) + .one(self.db.as_ref()) + .await? + .and_then(|r| r.count) + .unwrap_or(0); + + let mut values = base_values; + let limit_param = values.len() + 1; values.push((limit as i64).into()); + let offset_param = values.len() + 1; + values.push((offset as i64).into()); let sql = format!( r#"SELECT @@ -576,8 +637,8 @@ impl UsageService { FROM ai_usage_logs WHERE {} ORDER BY timestamp DESC - LIMIT ${}"#, - where_clause, next_param + LIMIT ${} OFFSET ${}"#, + where_clause, limit_param, offset_param ); let rows = UsageLogRow::find_by_statement(Statement::from_sql_and_values( @@ -588,7 +649,10 @@ impl UsageService { .all(self.db.as_ref()) .await?; - Ok(rows.into_iter().map(usage_log_from_row).collect()) + Ok(UsageLogPage { + entries: rows.into_iter().map(usage_log_from_row).collect(), + total, + }) } /// List conversations with aggregated stats. @@ -736,9 +800,64 @@ impl UsageService { if let Some(ref provider) = filter.provider { conditions.push(format!("provider = ${}", param_idx)); values.push(provider.clone().into()); - let _ = param_idx; // suppress unused warning + param_idx += 1; + } + + if let Some(status) = filter.status { + conditions.push(format!("status = ${}", param_idx)); + values.push(status.into()); + param_idx += 1; } + if let Some(cost) = filter.cost_gte { + conditions.push(format!("estimated_cost_microcents >= ${}", param_idx)); + values.push(cost.into()); + param_idx += 1; + } + + if let Some(cost) = filter.cost_gt { + conditions.push(format!("estimated_cost_microcents > ${}", param_idx)); + values.push(cost.into()); + param_idx += 1; + } + + if let Some(cost) = filter.cost_lte { + conditions.push(format!("estimated_cost_microcents <= ${}", param_idx)); + values.push(cost.into()); + param_idx += 1; + } + + if let Some(cost) = filter.cost_lt { + conditions.push(format!("estimated_cost_microcents < ${}", param_idx)); + values.push(cost.into()); + param_idx += 1; + } + + if let Some(tokens) = filter.tokens_gte { + conditions.push(format!("(input_tokens + output_tokens) >= ${}", param_idx)); + values.push(tokens.into()); + param_idx += 1; + } + + if let Some(tokens) = filter.tokens_gt { + conditions.push(format!("(input_tokens + output_tokens) > ${}", param_idx)); + values.push(tokens.into()); + param_idx += 1; + } + + if let Some(tokens) = filter.tokens_lte { + conditions.push(format!("(input_tokens + output_tokens) <= ${}", param_idx)); + values.push(tokens.into()); + param_idx += 1; + } + + if let Some(tokens) = filter.tokens_lt { + conditions.push(format!("(input_tokens + output_tokens) < ${}", param_idx)); + values.push(tokens.into()); + param_idx += 1; + } + + let _ = param_idx; // param_idx is reserved for any future conditions (conditions.join(" AND "), values) } } @@ -883,6 +1002,7 @@ mod tests { tags: Some("agent:support".to_string()), model: Some("claude-sonnet-4-6".to_string()), provider: Some("anthropic".to_string()), + ..Default::default() }; let (clause, values) = service.build_filter_clause(from, to, &filter); @@ -894,6 +1014,47 @@ mod tests { assert_eq!(values.len(), 7); } + #[test] + fn test_build_filter_clause_with_status_and_cost_bounds() { + let db = sea_orm::DatabaseConnection::Disconnected; + let service = UsageService::new(Arc::new(db)); + let from = Utc::now() - chrono::Duration::hours(1); + let to = Utc::now(); + let filter = UsageFilter { + provider: Some("openai".to_string()), + status: Some(429), + cost_gte: Some(100), + cost_lt: Some(50_000), + ..Default::default() + }; + + let (clause, values) = service.build_filter_clause(from, to, &filter); + // $1/$2 are the time range; provider, status, cost_gte, cost_lt follow. + assert!(clause.contains("provider = $3")); + assert!(clause.contains("status = $4")); + assert!(clause.contains("estimated_cost_microcents >= $5")); + assert!(clause.contains("estimated_cost_microcents < $6")); + assert_eq!(values.len(), 6); + } + + #[test] + fn test_build_filter_clause_with_token_bounds() { + let db = sea_orm::DatabaseConnection::Disconnected; + let service = UsageService::new(Arc::new(db)); + let from = Utc::now() - chrono::Duration::hours(1); + let to = Utc::now(); + let filter = UsageFilter { + tokens_gte: Some(500), + tokens_lt: Some(10_000), + ..Default::default() + }; + + let (clause, values) = service.build_filter_clause(from, to, &filter); + assert!(clause.contains("(input_tokens + output_tokens) >= $3")); + assert!(clause.contains("(input_tokens + output_tokens) < $4")); + assert_eq!(values.len(), 4); + } + #[test] fn test_usage_log_from_row_with_context() { let row = UsageLogRow { diff --git a/crates/temps-analytics-events/src/services/events_service.rs b/crates/temps-analytics-events/src/services/events_service.rs index 01e16f79..882e9c45 100644 --- a/crates/temps-analytics-events/src/services/events_service.rs +++ b/crates/temps-analytics-events/src/services/events_service.rs @@ -1408,6 +1408,15 @@ WHERE project_id = $1 let utm_campaign = utm_params.utm_campaign; let utm_term = utm_params.utm_term; let utm_content = utm_params.utm_content; + + // Parse user agent up front: the bot flag is needed both for the + // visitor record (so live-visitor lists can exclude crawlers) and the + // event row (so analytics read filters on `is_crawler` work). + let parsed_ua = + crate::services::user_agent::ParsedUserAgent::from_user_agent(user_agent.as_deref()); + let is_crawler = parsed_ua.is_bot(); + let crawler_name = parsed_ua.crawler_name(); + // Get visitor from visitor_id from visitors table // Convert visitor_id (String) to Option by looking up the visitor in the database let visitor_id_i32 = if let Some(ref visitor_id) = visitor_id { @@ -1433,6 +1442,11 @@ WHERE project_id = $1 if !v.has_activity { active_visitor.has_activity = sea_orm::ActiveValue::Set(true); } + // Flag the visitor as a crawler once any bot-UA event is seen. + // Only escalate (false -> true), never clear it. + if is_crawler && !v.is_crawler { + active_visitor.is_crawler = sea_orm::ActiveValue::Set(true); + } let _ = active_visitor.update(self.db.as_ref()).await; } @@ -1441,9 +1455,7 @@ WHERE project_id = $1 None }; - // Parse user agent for browser/OS info - let parsed_ua = - crate::services::user_agent::ParsedUserAgent::from_user_agent(user_agent.as_deref()); + // Browser/OS fields from the user agent parsed above. let browser = parsed_ua.browser; let browser_version = parsed_ua.browser_version; let operating_system = parsed_ua.operating_system; @@ -1496,7 +1508,8 @@ WHERE project_id = $1 is_entry: Set(false), is_exit: Set(false), is_bounce: Set(false), - is_crawler: Set(false), + is_crawler: Set(is_crawler), + crawler_name: Set(crawler_name), ..Default::default() }; @@ -2543,4 +2556,189 @@ mod tests { println!(" - Counts accurate for existing data"); println!(" - Zero counts for missing hours"); } + + /// Verifies that `record_event` persists the bot/crawler classification + /// derived from the User-Agent: a bot UA sets `is_crawler = true` with a + /// `crawler_name`, while a real browser UA leaves `is_crawler = false`. + #[tokio::test] + async fn test_record_event_persists_crawler_flag() { + use sea_orm::{ActiveModelTrait, Set}; + use temps_database::test_utils::TestDatabase; + use temps_entities::{ + deployments, environments, projects, source_type::SourceType, + upstream_config::UpstreamList, + }; + + let test_db: TestDatabase = match TestDatabase::with_migrations().await { + Ok(db) => db, + Err(e) => { + println!("Database not available, skipping test: {}", e); + return; + } + }; + let db = test_db.connection_arc(); + + let project = projects::ActiveModel { + name: Set("crawler-test".to_string()), + repo_name: Set("test-repo".to_string()), + repo_owner: Set("test-owner".to_string()), + directory: Set("/".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::NextJs), + preset_config: Set(None), + deployment_config: Set(None), + slug: Set("crawler-test".to_string()), + is_deleted: Set(false), + deleted_at: Set(None), + last_deployment: Set(None), + is_public_repo: Set(false), + git_url: Set(None), + git_provider_connection_id: Set(None), + attack_mode: Set(false), + enable_preview_environments: Set(false), + source_type: Set(SourceType::Git), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + ..Default::default() + } + .insert(db.as_ref()) + .await + .expect("Failed to insert test project"); + + let environment = environments::ActiveModel { + project_id: Set(project.id), + name: Set("production".to_string()), + branch: Set(Some("main".to_string())), + slug: Set("production".to_string()), + subdomain: Set("prod".to_string()), + host: Set(String::new()), + upstreams: Set(UpstreamList::new()), + is_preview: Set(false), + current_deployment_id: Set(None), + deleted_at: Set(None), + deployment_config: Set(None), + last_deployment: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + ..Default::default() + } + .insert(db.as_ref()) + .await + .expect("Failed to insert test environment"); + + let deployment = deployments::ActiveModel { + project_id: Set(project.id), + environment_id: Set(environment.id), + slug: Set(format!("test-deploy-{}", uuid::Uuid::new_v4())), + state: Set("ready".to_string()), + metadata: Set(Some(deployments::DeploymentMetadata::default())), + deploying_at: Set(None), + ready_at: Set(Some(chrono::Utc::now())), + started_at: Set(Some(chrono::Utc::now())), + finished_at: Set(Some(chrono::Utc::now())), + context_vars: Set(None), + branch_ref: Set(Some("main".to_string())), + tag_ref: Set(None), + commit_sha: Set(None), + commit_message: Set(None), + commit_author: Set(None), + commit_json: Set(None), + cancelled_reason: Set(None), + static_dir_location: Set(None), + screenshot_location: Set(None), + image_name: Set(None), + deployment_config: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + ..Default::default() + } + .insert(db.as_ref()) + .await + .expect("Failed to insert test deployment"); + + let service = AnalyticsEventsService::new(db.clone()); + + // A bot UA must be flagged as a crawler with a matched name. + let bot_ua = "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)"; + let bot_event = service + .record_event( + project.id, + Some(environment.id), + Some(deployment.id), + Some("bot-session".to_string()), + None, + "page_view", + serde_json::json!({}), + "/blog/bot-test", + "", + None, + None, + None, + None, + None, + None, + None, + Some(bot_ua.to_string()), + None, + None, + None, + None, + None, + None, + None, + ) + .await + .expect("Failed to record bot event"); + + assert!(bot_event.is_crawler, "bot UA should set is_crawler = true"); + assert_eq!( + bot_event.crawler_name, + Some("claudebot".to_string()), + "bot UA should record the matched crawler name" + ); + + // A real browser UA must not be flagged. + let human_ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \ + AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; + let human_event = service + .record_event( + project.id, + Some(environment.id), + Some(deployment.id), + Some("human-session".to_string()), + None, + "page_view", + serde_json::json!({}), + "/blog/human-test", + "", + None, + None, + None, + None, + None, + None, + None, + Some(human_ua.to_string()), + None, + None, + None, + None, + None, + None, + None, + ) + .await + .expect("Failed to record human event"); + + assert!( + !human_event.is_crawler, + "real browser UA should leave is_crawler = false" + ); + assert_eq!( + human_event.crawler_name, None, + "real browser UA should have no crawler name" + ); + + println!("✅ record_event crawler-flag persistence test passed!"); + } } diff --git a/crates/temps-analytics-events/src/services/user_agent.rs b/crates/temps-analytics-events/src/services/user_agent.rs index 4d2a1c3f..43509185 100644 --- a/crates/temps-analytics-events/src/services/user_agent.rs +++ b/crates/temps-analytics-events/src/services/user_agent.rs @@ -1,5 +1,83 @@ use woothee::parser::{Parser, WootheeResult}; +/// Substring patterns (lowercased) identifying bots, crawlers, scrapers, and +/// link-preview unfurlers that woothee's crawler list does not reliably catch. +/// Covers modern AI crawlers and headless browsers — the blog is a frequent +/// target for these and they pollute analytics with zero-duration sessions. +const BOT_UA_PATTERNS: &[&str] = &[ + // AI crawlers + "gptbot", + "claudebot", + "anthropic-ai", + "claude-web", + "perplexitybot", + "ccbot", + "bytespider", + "google-extended", + "applebot", + "amazonbot", + "meta-externalagent", + "oai-searchbot", + // Search / SEO crawlers + "googlebot", + "bingbot", + "yandexbot", + "duckduckbot", + "baiduspider", + "ahrefsbot", + "semrushbot", + "mj12bot", + "dotbot", + // Link-preview unfurlers + "facebookexternalhit", + "slackbot", + "discordbot", + "twitterbot", + "linkedinbot", + "whatsapp", + "telegrambot", + "pinterest", + "redditbot", + // Headless browsers / automation + "headlesschrome", + "phantomjs", + "puppeteer", + "playwright", + "selenium", + // Monitoring / generic + "pingdom", + "uptimerobot", + "statuscake", + "python-requests", + "curl/", + "wget/", + "go-http-client", + "node-fetch", + "axios/", + // Generic catch-alls (kept last) + "bot", + "crawler", + "spider", +]; + +/// Detect whether a raw user-agent string belongs to a known bot/crawler. +/// An empty or missing UA is treated as a bot — real browsers always send one. +/// Returns the matched pattern name, or `None` for human traffic. +fn detect_bot(user_agent: Option<&str>) -> Option { + let Some(ua) = user_agent else { + return Some("unknown".to_string()); + }; + let trimmed = ua.trim(); + if trimmed.is_empty() { + return Some("unknown".to_string()); + } + let lower = trimmed.to_lowercase(); + BOT_UA_PATTERNS + .iter() + .find(|pat| lower.contains(*pat)) + .map(|pat| pat.to_string()) +} + #[derive(Debug, Clone, Default)] pub struct ParsedUserAgent { pub browser: Option, @@ -7,24 +85,51 @@ pub struct ParsedUserAgent { pub operating_system: Option, pub operating_system_version: Option, pub device_type: Option, + /// The matched bot/crawler name, if this UA was identified as non-human. + pub crawler_name: Option, } impl ParsedUserAgent { /// Parse user agent string and extract browser information pub fn from_user_agent(user_agent: Option<&str>) -> Self { + let crawler_name = detect_bot(user_agent); + let Some(ua) = user_agent else { - return Self::default(); + return Self { + crawler_name, + ..Self::default() + }; }; if ua.trim().is_empty() { - return Self::default(); + return Self { + crawler_name, + ..Self::default() + }; } let parser = Parser::new(); - match parser.parse(ua) { + let mut parsed = match parser.parse(ua) { Some(result) => Self::from_woothee_result(&result), None => Self::default(), - } + }; + // Our substring match is the most specific signal and wins. woothee's + // own crawler classification is only a fallback when the substring + // list didn't match. + parsed.crawler_name = crawler_name.or_else(|| { + (parsed.device_type.as_deref() == Some("Bot")).then(|| "crawler".to_string()) + }); + parsed + } + + /// Whether this user agent was identified as a bot, crawler, or scraper. + pub fn is_bot(&self) -> bool { + self.crawler_name.is_some() + } + + /// The matched bot/crawler name, if any. + pub fn crawler_name(&self) -> Option { + self.crawler_name.clone() } fn from_woothee_result(result: &WootheeResult) -> Self { @@ -34,6 +139,7 @@ impl ParsedUserAgent { operating_system: Self::clean_name(result.os), operating_system_version: Self::clean_version(&result.os_version), device_type: Self::determine_device_type(result.category), + crawler_name: None, } } @@ -126,4 +232,65 @@ mod tests { assert_eq!(info.operating_system, Some("Android".to_string())); assert_eq!(info.device_type, Some("Mobile".to_string())); } + + #[test] + fn test_gptbot_is_bot() { + let ua = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(info.is_bot()); + assert_eq!(info.crawler_name(), Some("gptbot".to_string())); + } + + #[test] + fn test_claudebot_is_bot() { + let ua = "Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(info.is_bot()); + assert_eq!(info.crawler_name(), Some("claudebot".to_string())); + } + + #[test] + fn test_facebook_unfurler_is_bot() { + let ua = "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(info.is_bot()); + assert_eq!(info.crawler_name(), Some("facebookexternalhit".to_string())); + } + + #[test] + fn test_headless_chrome_is_bot() { + let ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/119.0.0.0 Safari/537.36"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(info.is_bot()); + assert_eq!(info.crawler_name(), Some("headlesschrome".to_string())); + } + + #[test] + fn test_empty_ua_is_bot() { + assert!(ParsedUserAgent::from_user_agent(None).is_bot()); + assert!(ParsedUserAgent::from_user_agent(Some("")).is_bot()); + assert!(ParsedUserAgent::from_user_agent(Some(" ")).is_bot()); + } + + #[test] + fn test_real_chrome_is_not_bot() { + let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(!info.is_bot()); + assert_eq!(info.crawler_name(), None); + } + + #[test] + fn test_real_safari_mobile_is_not_bot() { + let ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(!info.is_bot()); + } + + #[test] + fn test_googlebot_caught_by_substring() { + let ua = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"; + let info = ParsedUserAgent::from_user_agent(Some(ua)); + assert!(info.is_bot()); + } } diff --git a/crates/temps-analytics-session-replay/src/services/service.rs b/crates/temps-analytics-session-replay/src/services/service.rs index d4f09e9d..cc3c28cf 100644 --- a/crates/temps-analytics-session-replay/src/services/service.rs +++ b/crates/temps-analytics-session-replay/src/services/service.rs @@ -738,28 +738,22 @@ impl SessionReplayService { project_id, environment_id ); - // Build filtered base for total count (exclude 0s duration replays) + // Build filtered base for total count. Exclude replays with no + // measurable duration: both 0ms and NULL (never-finalized sessions, + // typically single-burst bot traffic) have nothing to play back. let mut count_select = session_replay_sessions::Entity::find() .filter(session_replay_sessions::Column::ProjectId.eq(project_id)) - .filter( - session_replay_sessions::Column::Duration - .is_null() - .or(session_replay_sessions::Column::Duration.gt(0)), - ); + .filter(session_replay_sessions::Column::Duration.gt(0)); if let Some(env_id) = environment_id { count_select = count_select.filter(session_replay_sessions::Column::EnvironmentId.eq(env_id)); } let total_count: u64 = count_select.count(self.db.as_ref()).await?; - // Use SeaORM query builder (exclude 0s duration replays) + // Same duration filter as the count query above — must stay in sync. let mut query = session_replay_sessions::Entity::find() .filter(session_replay_sessions::Column::ProjectId.eq(project_id)) - .filter( - session_replay_sessions::Column::Duration - .is_null() - .or(session_replay_sessions::Column::Duration.gt(0)), - ) + .filter(session_replay_sessions::Column::Duration.gt(0)) .inner_join(visitor::Entity) .join( sea_orm::JoinType::LeftJoin, @@ -953,7 +947,7 @@ impl SessionReplayService { FROM session_replay_sessions s INNER JOIN visitor v ON s.visitor_id = v.id LEFT JOIN ip_geolocations g ON v.ip_address_id = g.id - WHERE s.visitor_id = $1 AND (s.duration IS NULL OR s.duration > 0) + WHERE s.visitor_id = $1 AND s.duration > 0 ORDER BY s.created_at DESC LIMIT {} OFFSET {} "#, diff --git a/crates/temps-analytics/src/analytics.rs b/crates/temps-analytics/src/analytics.rs index 9b2201d6..c478435c 100644 --- a/crates/temps-analytics/src/analytics.rs +++ b/crates/temps-analytics/src/analytics.rs @@ -1129,7 +1129,18 @@ impl Analytics for AnalyticsService { (ARRAY_AGG(e.page_path ORDER BY e.timestamp DESC))[1] as exit_path, MIN(e.referrer) as referrer, BOOL_OR(e.is_bounce) as is_bounced, - COUNT(*) FILTER (WHERE e.event_type NOT IN ('page_view', 'page_leave')) > 0 as is_engaged + -- A session is engaged if the visitor spent real attention: + -- at least 10s of measured wall-clock time, OR fired a + -- genuine interaction event. Auto-fired view events + -- (page_view, page_leave, *_viewed) do not count — they + -- trigger from intersection observers for bots too. + ( + EXTRACT(EPOCH FROM (MAX(e.timestamp) - MIN(e.timestamp))) >= 10 + OR COUNT(*) FILTER ( + WHERE e.event_type NOT IN ('page_view', 'page_leave') + AND e.event_type NOT LIKE '%\_viewed' ESCAPE '\' + ) > 0 + ) as is_engaged FROM events e LEFT JOIN request_logs rl ON rl.session_id = e.id AND rl.project_id = e.project_id LEFT JOIN request_sessions rs ON rs.session_Id = e.session_id @@ -1570,8 +1581,16 @@ impl Analytics for AnalyticsService { -- Calculate bounce (1 or fewer page views) (SELECT COUNT(*) FROM events e WHERE e.session_id = rs.session_id AND COALESCE(e.event_name, e.event_type, 'page_view') = 'page_view') <= 1 as is_bounced, - -- Calculate engagement (any non-page_view/page_leave events) - (SELECT COUNT(*) > 0 FROM events e WHERE e.session_id = rs.session_id AND COALESCE(e.event_name, e.event_type, '') NOT IN ('page_view', 'page_leave', '')) as is_engaged + -- Engaged if the visitor spent >= 10s of measured time, OR + -- fired a genuine interaction event. Auto-fired view events + -- (page_view, page_leave, *_viewed) are excluded — they fire + -- from intersection observers for bots too. + ( + EXTRACT(EPOCH FROM (rs.last_accessed_at - rs.started_at)) >= 10 + OR (SELECT COUNT(*) > 0 FROM events e WHERE e.session_id = rs.session_id + AND COALESCE(e.event_name, e.event_type, '') NOT IN ('page_view', 'page_leave', '') + AND COALESCE(e.event_name, e.event_type, '') NOT LIKE '%\_viewed' ESCAPE '\') + ) as is_engaged FROM request_sessions rs WHERE rs.id = $1 diff --git a/crates/temps-backup/src/engines/v2_common.rs b/crates/temps-backup/src/engines/v2_common.rs index 7eefe7cc..85f94531 100644 --- a/crates/temps-backup/src/engines/v2_common.rs +++ b/crates/temps-backup/src/engines/v2_common.rs @@ -58,6 +58,97 @@ pub(crate) fn bundled_roots_http_client() -> SharedHttpClient { .clone() } +/// Format an AWS SDK error into something a human can act on. +/// +/// `Display` on `SdkError` collapses to a useless one-liner like +/// `service error` for any 4xx/5xx — it doesn't include the status code, +/// the request id (which Cloudflare R2/AWS support needs), the +/// service-specific error code (`AccessDenied`, `NoSuchBucket`, …), or +/// the response body. Operators staring at a failed backup deserve all +/// of those; this helper pulls them out via the typed +/// `ProvideErrorMetadata` trait and falls back to `Debug` for +/// transport-layer errors that don't carry SDK metadata. +/// +/// Returned string is the operator-facing description; goes verbatim into +/// `backups.error_message` and bubbles up through the UI. +pub fn describe_sdk_error(op: &str, err: &aws_sdk_s3::error::SdkError) -> String +where + E: std::fmt::Debug + aws_sdk_s3::error::ProvideErrorMetadata, +{ + use aws_sdk_s3::error::SdkError; + use aws_sdk_s3::operation::RequestId; + + // Pieces we'll join with " | " so a single-line DB column stays + // readable. Only push parts that actually carry information. + let mut parts: Vec = Vec::new(); + parts.push(format!("{} failed", op)); + + match err { + SdkError::ConstructionFailure(_) => { + parts.push("request construction failure".into()); + } + SdkError::TimeoutError(_) => { + parts.push("request timed out (operation-level)".into()); + } + SdkError::DispatchFailure(d) => { + // Network / TLS / DNS. Display gives "dispatch failure"; the + // wrapped error has the actual cause. + parts.push(format!("dispatch failure: {:?}", d)); + } + SdkError::ResponseError(r) => { + // Could not even parse the HTTP response. Surface what we have. + parts.push(format!("invalid response: {:?}", r)); + } + SdkError::ServiceError(s) => { + // Typed service error: 4xx/5xx with a parsed XML body. + let raw = s.err(); + let resp = s.raw(); + parts.push(format!("HTTP {}", resp.status().as_u16())); + if let Some(code) = raw.code() { + parts.push(format!("code={}", code)); + } + if let Some(msg) = raw.message() { + parts.push(format!("message={}", msg)); + } + if let Some(rid) = raw.meta().request_id() { + parts.push(format!("request_id={}", rid)); + } + // Extended request id (`x-amz-id-2`) — AWS support asks for + // this. Cloudflare R2 doesn't emit one, so it's optional. + if let Some(eid) = resp.headers().get("x-amz-id-2") { + parts.push(format!("extended_request_id={}", eid)); + } + // Last resort: include the (truncated) response body so the + // raw XML/JSON is visible. Storage providers sometimes put + // diagnostic detail there that the SDK doesn't surface as + // typed fields. + if let Some(body_bytes) = resp.body().bytes() { + if !body_bytes.is_empty() { + let body_str = String::from_utf8_lossy(body_bytes); + let trimmed = body_str.trim(); + if !trimmed.is_empty() { + const MAX_BODY: usize = 512; + let body_excerpt: String = if trimmed.chars().count() > MAX_BODY { + let mut s: String = trimmed.chars().take(MAX_BODY).collect(); + s.push('…'); + s + } else { + trimmed.to_string() + }; + parts.push(format!("body={}", body_excerpt)); + } + } + } + } + _ => { + // Future-proof: SdkError is #[non_exhaustive]. + parts.push(format!("{:?}", err)); + } + } + + parts.join(" | ") +} + /// Multipart upload threshold. Files larger than this use multipart /// upload instead of a single PUT. pub const MULTIPART_THRESHOLD: i64 = 30 * 1024 * 1024; @@ -117,28 +208,109 @@ impl BackupTags { tags } - /// Render the tag set as the `Tagging` HTTP header / SDK param - /// (`k1=v1&k2=v2`, URL-encoded). Every backup carries - /// `temps-managed=true` so lifecycle rules can target only objects - /// temps wrote. - pub fn to_tagging_string(&self) -> String { - let mut parts: Vec = Vec::with_capacity(4); - parts.push("temps-managed=true".to_string()); + /// Structured form of the tag set. Used by the post-upload + /// `PutObjectTagging` path (see `apply_object_tags`) because some + /// S3-compatible stores — notably Cloudflare R2 — reject the + /// `x-amz-tagging` request header on PutObject / CreateMultipartUpload + /// with `501 NotImplemented`. Applying tags as a separate call works + /// everywhere, which is why this is the only tag-rendering path: do + /// not re-introduce a `to_tagging_string` helper for the upload header. + pub fn to_tag_pairs(&self) -> Vec<(String, String)> { + let mut pairs: Vec<(String, String)> = Vec::with_capacity(4); + pairs.push(("temps-managed".to_string(), "true".to_string())); match self.retention_days { Some(days) if days > 0 => { - parts.push(format!("temps-retention-days={}", days)); + pairs.push(("temps-retention-days".to_string(), days.to_string())); } _ => { - parts.push("temps-retention-days=never".to_string()); + pairs.push(("temps-retention-days".to_string(), "never".to_string())); } } if let Some(id) = self.schedule_id { - parts.push(format!("temps-schedule-id={}", id)); + pairs.push(("temps-schedule-id".to_string(), id.to_string())); } if let Some(id) = self.backup_id { - parts.push(format!("temps-backup-id={}", id)); + pairs.push(("temps-backup-id".to_string(), id.to_string())); + } + pairs + } +} + +/// Apply tags to an S3 object **after** upload via `PutObjectTagging`. +/// +/// History: we originally passed the tag set as the `Tagging` header on +/// the upload call itself. Cloudflare R2 returns `501 NotImplemented` on +/// that header for both `PutObject` and `CreateMultipartUpload`. Moving +/// to a follow-up `PutObjectTagging` call didn't help either — R2 +/// returns the same `501 NotImplemented` on `PutObjectTagging`. Object +/// tagging is simply not implemented on R2. +/// +/// So this call is **best-effort**: if the provider rejects it with a +/// "not implemented / not supported" style error, we log a warning and +/// continue. The backup data is already uploaded and tracked in our DB, +/// and app-side `enforce_retention` handles cleanup regardless. The only +/// thing that gets disabled on tag-less providers is the bucket-side +/// `BucketLifecycleConfiguration` reconciler that depends on tag filters +/// — which is also already best-effort (see `s3_lifecycle.rs`). +/// +/// On AWS S3 / MinIO / any compliant store this still applies tags +/// normally and fails the backup if tagging is genuinely broken (auth, +/// network, etc.) so we don't silently drop diagnostic plumbing. +pub async fn apply_object_tags( + client: &S3Client, + bucket: &str, + key: &str, + tags: &BackupTags, +) -> Result<(), BackupError> { + let mut tag_set_builder = aws_sdk_s3::types::Tagging::builder(); + for (k, v) in tags.to_tag_pairs() { + let tag = aws_sdk_s3::types::Tag::builder() + .key(k) + .value(v) + .build() + .map_err(|e| BackupError::Failed { + reason: format!("failed to build tag for s3://{}/{}: {}", bucket, key, e), + })?; + tag_set_builder = tag_set_builder.tag_set(tag); + } + let tagging = tag_set_builder.build().map_err(|e| BackupError::Failed { + reason: format!( + "failed to build Tagging payload for s3://{}/{}: {}", + bucket, key, e + ), + })?; + + match client + .put_object_tagging() + .bucket(bucket) + .key(key) + .tagging(tagging) + .send() + .await + { + Ok(_) => Ok(()), + Err(e) => { + let detail = describe_sdk_error( + &format!("put_object_tagging on s3://{}/{}", bucket, key), + &e, + ); + if crate::services::s3_lifecycle::is_unsupported_error(&detail) { + // Cloudflare R2 (and any other store without + // PutObjectTagging) lands here. Don't fail the backup; + // app-side retention (see `BackupService::enforce_retention`) + // is the source of truth on these providers. + warn!( + target: "temps_backup::tagging", + bucket = bucket, + key = key, + detail = %detail, + "S3 provider does not support PutObjectTagging — object stored, tags skipped; relying on app-side retention", + ); + Ok(()) + } else { + Err(BackupError::Failed { reason: detail }) + } } - parts.join("&") } } @@ -233,7 +405,7 @@ pub async fn assert_bucket_reachable(client: &S3Client, bucket: &str) -> Result< .send() .await .map_err(|e| BackupError::Failed { - reason: format!("S3 bucket '{}' is not reachable: {}", bucket, e), + reason: describe_sdk_error(&format!("head_bucket on '{}'", bucket), &e), })?; Ok(()) } @@ -318,21 +490,27 @@ pub async fn upload_single_part( reason: format!("failed to create byte stream from {}: {}", path, e), })?; - let mut req = client + // Tags are applied via PutObjectTagging *after* the upload — see + // `apply_object_tags` for the R2-compatibility rationale. We + // deliberately do not pass `.tagging(...)` here. + client .put_object() .bucket(bucket) .key(key) .body(body) - .content_type(content_type); + .content_type(content_type) + .send() + .await + .map_err(|e| BackupError::Failed { + reason: describe_sdk_error( + &format!("single-part upload to s3://{}/{}", bucket, key), + &e, + ), + })?; + if let Some(tags) = tags { - req = req.tagging(tags.to_tagging_string()); + apply_object_tags(client, bucket, key, tags).await?; } - req.send().await.map_err(|e| BackupError::Failed { - reason: format!( - "single-part upload to s3://{}/{} failed: {}", - bucket, key, e - ), - })?; Ok(()) } @@ -349,17 +527,23 @@ pub async fn upload_multipart( ) -> Result<(), BackupError> { use tokio_stream::StreamExt as TokioStreamExt; - let mut create_req = client + // Tags are applied via PutObjectTagging *after* the upload completes + // — see `apply_object_tags` for the R2-compatibility rationale. We + // deliberately do not pass `.tagging(...)` on the create call here; + // doing so makes Cloudflare R2 fail the upload with 501 NotImplemented. + let create_resp = client .create_multipart_upload() .bucket(bucket) .key(key) - .content_type(content_type); - if let Some(tags) = tags { - create_req = create_req.tagging(tags.to_tagging_string()); - } - let create_resp = create_req.send().await.map_err(|e| BackupError::Failed { - reason: format!("create_multipart_upload failed: {}", e), - })?; + .content_type(content_type) + .send() + .await + .map_err(|e| BackupError::Failed { + reason: describe_sdk_error( + &format!("create_multipart_upload for s3://{}/{}", bucket, key), + &e, + ), + })?; let upload_id = create_resp.upload_id().ok_or_else(|| BackupError::Failed { reason: "create_multipart_upload returned no upload_id".into(), @@ -400,7 +584,10 @@ pub async fn upload_multipart( .map_err(|e| { abort_multipart_detached(client.clone(), bucket, key, upload_id); BackupError::Failed { - reason: format!("upload_part {} failed: {}", part_number, e), + reason: describe_sdk_error( + &format!("upload_part {} for s3://{}/{}", part_number, bucket, key), + &e, + ), } })?; @@ -426,7 +613,13 @@ pub async fn upload_multipart( .map_err(|e| { abort_multipart_detached(client.clone(), bucket, key, upload_id); BackupError::Failed { - reason: format!("upload_part {} (final) failed: {}", part_number, e), + reason: describe_sdk_error( + &format!( + "upload_part {} (final) for s3://{}/{}", + part_number, bucket, key + ), + &e, + ), } })?; let completed_part = aws_sdk_s3::types::CompletedPart::builder() @@ -445,9 +638,15 @@ pub async fn upload_multipart( .send() .await .map_err(|e| BackupError::Failed { - reason: format!("complete_multipart_upload failed: {}", e), + reason: describe_sdk_error( + &format!("complete_multipart_upload for s3://{}/{}", bucket, key), + &e, + ), })?; + if let Some(tags) = tags { + apply_object_tags(client, bucket, key, tags).await?; + } Ok(()) } @@ -532,9 +731,9 @@ pub async fn write_metadata_companion( .send() .await .map_err(|e| BackupError::Failed { - reason: format!( - "failed to upload metadata.json to s3://{}/{}: {}", - bucket, metadata_key, e + reason: describe_sdk_error( + &format!("metadata.json upload to s3://{}/{}", bucket, metadata_key), + &e, ), })?; Ok(()) diff --git a/crates/temps-backup/src/handlers/audit.rs b/crates/temps-backup/src/handlers/audit.rs index 64cd7fe2..3bfc8c9f 100644 --- a/crates/temps-backup/src/handlers/audit.rs +++ b/crates/temps-backup/src/handlers/audit.rs @@ -247,6 +247,75 @@ impl AuditOperation for ExternalServiceBackupRunAudit { } } +/// Audit record emitted when external services are attached to a backup +/// schedule via `POST /api/backups/schedules/{id}/services`. +#[derive(Debug, Clone, Serialize)] +pub struct ScheduleServicesAttachedAudit { + pub context: AuditContext, + pub schedule_id: i32, + /// IDs that the caller requested be attached (after dedup). + pub requested_service_ids: Vec, + /// Number of rows actually inserted (post `ON CONFLICT DO NOTHING`). + pub inserted_count: u64, +} + +impl AuditOperation for ScheduleServicesAttachedAudit { + fn operation_type(&self) -> String { + "BACKUP_SCHEDULE_SERVICES_ATTACHED".to_string() + } + + fn user_id(&self) -> i32 { + self.context.user_id + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| anyhow::anyhow!("Failed to serialize audit operation {}", e)) + } +} + +/// Audit record emitted when an external service is detached from a backup +/// schedule via `DELETE /api/backups/schedules/{id}/services/{service_id}`. +#[derive(Debug, Clone, Serialize)] +pub struct ScheduleServiceDetachedAudit { + pub context: AuditContext, + pub schedule_id: i32, + pub service_id: i32, + /// Whether a row was actually removed (false ⇒ idempotent no-op). + pub removed: bool, +} + +impl AuditOperation for ScheduleServiceDetachedAudit { + fn operation_type(&self) -> String { + "BACKUP_SCHEDULE_SERVICE_DETACHED".to_string() + } + + fn user_id(&self) -> i32 { + self.context.user_id + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> Result { + serde_json::to_string(self) + .map_err(|e| anyhow::anyhow!("Failed to serialize audit operation {}", e)) + } +} + /// Audit record emitted when an operator triggers an immediate (manual) run of /// a backup schedule via `POST /api/backups/schedules/{id}/run`. #[derive(Debug, Clone, Serialize)] diff --git a/crates/temps-backup/src/handlers/backup_handler.rs b/crates/temps-backup/src/handlers/backup_handler.rs index e60a5eed..7786a79f 100644 --- a/crates/temps-backup/src/handlers/backup_handler.rs +++ b/crates/temps-backup/src/handlers/backup_handler.rs @@ -2,7 +2,8 @@ use crate::engines::dispatch::{resolve_engine_key, ResolveEngineError}; use crate::handlers::audit::{ AuditContext, BackupRunAudit, BackupScheduleStatusChangedAudit, BackupScheduleUpdatedAudit, ExternalServiceBackupRunAudit, S3SourceCreatedAudit, S3SourceDeletedAudit, - S3SourceUpdatedAudit, ScheduleRunNowAudit, + S3SourceUpdatedAudit, ScheduleRunNowAudit, ScheduleServiceDetachedAudit, + ScheduleServicesAttachedAudit, }; use crate::handlers::types::BackupAppState; use crate::services::BackupTriggerParams; @@ -122,7 +123,11 @@ impl From for Problem { enable_backup_schedule, update_backup_schedule, run_external_service_backup, - list_backup_alerts + list_backup_alerts, + list_schedule_services, + attach_schedule_services, + detach_schedule_service, + list_service_schedules ), components( schemas( @@ -154,6 +159,8 @@ impl From for Problem { CancelBackupResponse, ChildBackupEntryResponse, ChildBackupListResponse, + AttachScheduleServicesRequest, + AttachScheduleServicesResponse, ) ), info( @@ -227,6 +234,18 @@ pub struct CreateBackupScheduleRequest { /// "use engine default." The per-job `max_runtime_secs` in /// `EnqueueJobParams` can still override this for ad-hoc triggers. pub max_runtime_secs: Option, + /// When `true` (default), the schedule backs up every external service + /// on the host — including databases created in the future. When + /// `false`, the schedule backs up only the services explicitly attached + /// via `POST /backups/schedules/{id}/services`. Omit to use the default. + #[serde(default)] + pub target_all_services: Option, + /// When `true` (default), every run also produces a `control_plane` + /// backup of Temps's own database. Operators who use Temps purely as + /// a backup orchestrator for external DBs can set this to `false` to + /// keep the run history focused on those services. + #[serde(default)] + pub include_control_plane: Option, } /// Deserializer for `Option>` that maps: @@ -278,6 +297,12 @@ pub struct UpdateBackupScheduleRequest { pub enabled: Option, /// Replace the full tag list. Skipped when `None`. pub tags: Option>, + /// Toggle between "back up every database" (`true`) and "back up only + /// the explicit list" (`false`). When set to `true`, the server clears + /// the explicit membership rows for this schedule. + pub target_all_services: Option, + /// Toggle whether the control-plane backup is produced on every run. + pub include_control_plane: Option, } /// Returns the names of fields that are present (i.e., `Some`) in the patch @@ -305,6 +330,12 @@ fn changed_fields_for_audit(request: &UpdateBackupScheduleRequest) -> Vec, + /// When `true`, the schedule auto-includes every external service on + /// the host (and any future ones). When `false`, the schedule only + /// targets services attached via `backup_schedule_services`. + pub target_all_services: bool, + /// When `true`, every run also produces a `control_plane` backup + /// (Temps's own Postgres). When `false`, only the external service + /// fan-out happens. + pub include_control_plane: bool, +} + +/// Body for `POST /api/backups/schedules/{id}/services` — attach external +/// services to a backup schedule. Idempotent. +#[derive(Debug, Deserialize, ToSchema)] +pub struct AttachScheduleServicesRequest { + /// External service ids to attach. Duplicates are de-duplicated server-side. + pub service_ids: Vec, +} + +/// Response for `POST /api/backups/schedules/{id}/services`. +#[derive(Debug, Serialize, ToSchema)] +pub struct AttachScheduleServicesResponse { + /// Number of rows actually inserted (excludes rows skipped by + /// `ON CONFLICT DO NOTHING`). + pub inserted: u64, + /// Total number of services now attached to the schedule. + pub total_attached: usize, } /// Summary of the external service that owns a backup. Only populated for @@ -651,6 +708,18 @@ impl From for BackupScheduleResponse { next_run: schedule.next_run.map(|dt| dt.timestamp_millis()), last_run: schedule.last_run.map(|dt| dt.timestamp_millis()), max_runtime_secs: schedule.max_runtime_secs, + target_all_services: schedule.target_all_services, + include_control_plane: schedule.include_control_plane, + } + } +} + +impl From for ExternalServiceSummary { + fn from(svc: temps_entities::external_services::Model) -> Self { + Self { + id: svc.id, + name: svc.name, + service_type: svc.service_type, } } } @@ -935,6 +1004,186 @@ async fn list_external_service_backups( Ok(Json(response)) } +/// List the external services attached to a backup schedule. +#[utoipa::path( + tag = "Backups", + get, + path = "/backups/schedules/{id}/services", + params(("id" = i32, Path, description = "Schedule ID")), + responses( + (status = 200, description = "Services attached to this schedule", body = Vec), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Schedule not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn list_schedule_services( + RequireAuth(auth): RequireAuth, + State(app_state): State>, + Path(id): Path, +) -> Result { + permission_guard!(auth, BackupsRead); + let services = app_state + .backup_service + .list_services_for_schedule(id) + .await + .map_err(Problem::from)?; + let body: Vec = services.into_iter().map(Into::into).collect(); + Ok(Json(body)) +} + +/// Attach one or more external services to a backup schedule. Idempotent — +/// services that are already attached are silently skipped (`ON CONFLICT +/// DO NOTHING`). Returns the count of newly inserted rows + the total +/// membership after the operation. +#[utoipa::path( + tag = "Backups", + post, + path = "/backups/schedules/{id}/services", + params(("id" = i32, Path, description = "Schedule ID")), + request_body = AttachScheduleServicesRequest, + responses( + (status = 200, description = "Services attached", body = AttachScheduleServicesResponse), + (status = 400, description = "Validation error", body = ProblemDetails), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Schedule not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn attach_schedule_services( + RequireAuth(auth): RequireAuth, + State(app_state): State>, + Extension(metadata): Extension, + Path(id): Path, + Json(request): Json, +) -> Result { + permission_guard!(auth, BackupsCreate); + + let inserted = app_state + .backup_service + .attach_services_to_schedule(id, &request.service_ids) + .await + .map_err(Problem::from)?; + + // Fetch the post-attach membership for the response so the UI doesn't + // have to issue a follow-up GET. + let total_attached = app_state + .backup_service + .list_services_for_schedule(id) + .await + .map_err(Problem::from)? + .len(); + + let audit = ScheduleServicesAttachedAudit { + context: AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + }, + schedule_id: id, + requested_service_ids: request.service_ids.clone(), + inserted_count: inserted, + }; + if let Err(e) = app_state.audit_service.create_audit_log(&audit).await { + error!( + "Failed to create audit log for attach_schedule_services: {}", + e + ); + } + + Ok(Json(AttachScheduleServicesResponse { + inserted, + total_attached, + })) +} + +/// Detach a single external service from a backup schedule. Idempotent — +/// returns `204` whether or not a row was actually removed. +#[utoipa::path( + tag = "Backups", + delete, + path = "/backups/schedules/{id}/services/{service_id}", + params( + ("id" = i32, Path, description = "Schedule ID"), + ("service_id" = i32, Path, description = "External service ID"), + ), + responses( + (status = 204, description = "Service detached (or was not attached)"), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn detach_schedule_service( + RequireAuth(auth): RequireAuth, + State(app_state): State>, + Extension(metadata): Extension, + Path((id, service_id)): Path<(i32, i32)>, +) -> Result { + permission_guard!(auth, BackupsDelete); + + let removed = app_state + .backup_service + .detach_service_from_schedule(id, service_id) + .await + .map_err(Problem::from)?; + + let audit = ScheduleServiceDetachedAudit { + context: AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + }, + schedule_id: id, + service_id, + removed, + }; + if let Err(e) = app_state.audit_service.create_audit_log(&audit).await { + error!( + "Failed to create audit log for detach_schedule_service: {}", + e + ); + } + + Ok(StatusCode::NO_CONTENT) +} + +/// List the schedules that target a specific external service. Useful for +/// the service detail page ("which schedules back this DB up?"). +#[utoipa::path( + tag = "Backups", + get, + path = "/backups/external-services/{service_id}/schedules", + params(("service_id" = i32, Path, description = "External service ID")), + responses( + (status = 200, description = "Schedules backing up this service", body = Vec), + (status = 401, description = "Unauthorized", body = ProblemDetails), + (status = 403, description = "Insufficient permissions", body = ProblemDetails), + (status = 404, description = "Service not found", body = ProblemDetails), + (status = 500, description = "Internal server error", body = ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn list_service_schedules( + RequireAuth(auth): RequireAuth, + State(app_state): State>, + Path(service_id): Path, +) -> Result { + permission_guard!(auth, BackupsRead); + let schedules = app_state + .backup_service + .list_schedules_for_service(service_id) + .await + .map_err(Problem::from)?; + let body: Vec = schedules.into_iter().map(Into::into).collect(); + Ok(Json(body)) +} + pub fn configure_routes() -> Router> { Router::new() .route( @@ -973,6 +1222,18 @@ pub fn configure_routes() -> Router> { ) .route("/backups/schedules/{id}/runs", get(list_schedule_runs)) .route("/backups/schedules/{id}/run", post(run_schedule_now)) + .route( + "/backups/schedules/{id}/services", + get(list_schedule_services).post(attach_schedule_services), + ) + .route( + "/backups/schedules/{id}/services/{service_id}", + axum::routing::delete(detach_schedule_service), + ) + .route( + "/backups/external-services/{service_id}/schedules", + get(list_service_schedules), + ) .route( "/backups/schedule-runs/{id}/jobs", get(list_schedule_run_jobs), diff --git a/crates/temps-backup/src/services/backup.rs b/crates/temps-backup/src/services/backup.rs index 161e2691..86d4fd9a 100644 --- a/crates/temps-backup/src/services/backup.rs +++ b/crates/temps-backup/src/services/backup.rs @@ -413,7 +413,10 @@ impl From, ) -> Self { - BackupError::S3(format!("Failed to put object: {}", err)) + BackupError::S3(crate::engines::v2_common::describe_sdk_error( + "put_object", + &err, + )) } } @@ -423,7 +426,10 @@ impl From, ) -> Self { - BackupError::S3(format!("Failed to delete object: {}", err)) + BackupError::S3(crate::engines::v2_common::describe_sdk_error( + "delete_object", + &err, + )) } } @@ -439,7 +445,10 @@ impl aws_sdk_s3::operation::complete_multipart_upload::CompleteMultipartUploadError, >, ) -> Self { - BackupError::S3(format!("Failed to complete multipart upload: {}", err)) + BackupError::S3(crate::engines::v2_common::describe_sdk_error( + "complete_multipart_upload", + &err, + )) } } @@ -3961,9 +3970,32 @@ impl BackupService { tags: Set(tags_json), next_run: Set(next_run), max_runtime_secs: Set(request.max_runtime_secs), + // Default is true ("back up every database, including future + // ones") so a freshly-created schedule does the obvious thing + // without the operator having to pick services up front. + target_all_services: Set(request.target_all_services.unwrap_or(true)), + include_control_plane: Set(request.include_control_plane.unwrap_or(true)), ..Default::default() }; + // Validate the resulting schedule has at least one thing to back + // up. We do this *after* defaulting so callers who omit the flags + // get the safe "back up everything" behaviour instead of a 400. + let target_all = request.target_all_services.unwrap_or(true); + let include_cp = request.include_control_plane.unwrap_or(true); + if !target_all && !include_cp { + // Without target_all_services the operator must also attach at + // least one service. They can't do that until the schedule + // exists, so the only way to get here legitimately is via an + // update — block it on create. + return Err(BackupError::Validation( + "A schedule must include the control plane, target all databases, \ + or both. Set include_control_plane=true or target_all_services=true \ + (or omit the flags to use the defaults)." + .to_string(), + )); + } + let schedule_model = new_schedule.insert(self.db.as_ref()).await?; info!("Created new backup schedule: {}", schedule_model.name); self.fire_lifecycle_reconcile(schedule_model.s3_source_id); @@ -4018,6 +4050,148 @@ impl BackupService { Ok(result.rows_affected > 0) } + /// Attach external services to a backup schedule. + /// + /// Idempotent: re-attaching an already-attached service is a no-op (rows + /// are inserted with `ON CONFLICT DO NOTHING`). Returns the number of rows + /// actually inserted. Validates that the schedule and every supplied + /// service id exist. + pub async fn attach_services_to_schedule( + &self, + schedule_id: i32, + service_ids: &[i32], + ) -> Result { + use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter}; + + // Validate schedule exists (raises NotFound otherwise). + self.get_backup_schedule(schedule_id).await?; + + if service_ids.is_empty() { + return Ok(0); + } + + // De-duplicate the input so we don't ask the DB to insert dup rows + // (ON CONFLICT handles it, but logging stays clean). + let mut unique_ids: Vec = service_ids.to_vec(); + unique_ids.sort_unstable(); + unique_ids.dedup(); + + // Validate every requested service id exists. + let found_count = temps_entities::external_services::Entity::find() + .filter(temps_entities::external_services::Column::Id.is_in(unique_ids.clone())) + .count(self.db.as_ref()) + .await?; + if (found_count as usize) != unique_ids.len() { + return Err(BackupError::Validation(format!( + "One or more service ids do not exist (requested {}, found {})", + unique_ids.len(), + found_count + ))); + } + + // Build a single multi-row INSERT with ON CONFLICT DO NOTHING for + // idempotency. Sea-ORM `insert_many` does not expose ON CONFLICT in + // a portable way, so we drop to raw SQL. + let mut sql = String::from( + "INSERT INTO backup_schedule_services (schedule_id, service_id, created_at) VALUES ", + ); + let mut params: Vec = Vec::with_capacity(unique_ids.len() * 2 + 1); + params.push(sea_orm::Value::from(schedule_id)); + for (idx, sid) in unique_ids.iter().enumerate() { + if idx > 0 { + sql.push_str(", "); + } + let p = idx + 2; // $1 = schedule_id, $2.. = service_ids + sql.push_str(&format!("($1, ${}, NOW())", p)); + params.push(sea_orm::Value::from(*sid)); + } + sql.push_str(" ON CONFLICT (schedule_id, service_id) DO NOTHING"); + + let result = self + .db + .execute(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + &sql, + params, + )) + .await + .map_err(BackupError::Database)?; + + Ok(result.rows_affected()) + } + + /// Detach a single external service from a backup schedule. + /// + /// Returns `true` if a row was removed, `false` if nothing was attached. + /// Does not raise `NotFound` when the membership row is absent — callers + /// can treat detach as idempotent. + pub async fn detach_service_from_schedule( + &self, + schedule_id: i32, + service_id: i32, + ) -> Result { + use sea_orm::EntityTrait; + + let result = temps_entities::backup_schedule_services::Entity::delete_by_id(( + schedule_id, + service_id, + )) + .exec(self.db.as_ref()) + .await + .map_err(BackupError::Database)?; + + Ok(result.rows_affected > 0) + } + + /// List the external services attached to a given schedule, ordered by + /// service name for stable UI rendering. Raises `NotFound` if the + /// schedule does not exist. + pub async fn list_services_for_schedule( + &self, + schedule_id: i32, + ) -> Result, BackupError> { + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + + self.get_backup_schedule(schedule_id).await?; + + let services = temps_entities::external_services::Entity::find() + .inner_join(temps_entities::backup_schedule_services::Entity) + .filter(temps_entities::backup_schedule_services::Column::ScheduleId.eq(schedule_id)) + .order_by_asc(temps_entities::external_services::Column::Name) + .all(self.db.as_ref()) + .await + .map_err(BackupError::Database)?; + + Ok(services) + } + + /// List the schedules that target a given external service. Raises + /// `NotFound` if the service does not exist. + pub async fn list_schedules_for_service( + &self, + service_id: i32, + ) -> Result, BackupError> { + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + + temps_entities::external_services::Entity::find_by_id(service_id) + .one(self.db.as_ref()) + .await? + .ok_or_else(|| BackupError::NotFound { + resource: "ExternalService".to_string(), + detail: format!("External service {} not found", service_id), + })?; + + let schedules = temps_entities::backup_schedules::Entity::find() + .inner_join(temps_entities::backup_schedule_services::Entity) + .filter(temps_entities::backup_schedule_services::Column::ServiceId.eq(service_id)) + .order_by_asc(temps_entities::backup_schedules::Column::Name) + .all(self.db.as_ref()) + .await + .map_err(BackupError::Database)?; + + Ok(schedules) + } + /// List backups for a schedule pub async fn list_backups_for_schedule( &self, @@ -4633,12 +4807,40 @@ SELECT sr.id FROM schedule_runs sr ), })?; - // Load external services upfront so we can resolve engine keys before - // opening the transaction. - let external_services = temps_entities::external_services::Entity::find() - .all(self.db.as_ref()) - .await - .map_err(BackupError::Database)?; + // Load the external services this schedule should fan out to. + // Two modes (set on `backup_schedules.target_all_services`): + // - true → every external service on the host (auto-includes + // future databases); the explicit join table is ignored. + // - false → only services attached via `backup_schedule_services` + // (the operator picked specific DBs). + use sea_orm::{ColumnTrait, QueryFilter}; + let external_services = if schedule.target_all_services { + temps_entities::external_services::Entity::find() + .all(self.db.as_ref()) + .await + .map_err(BackupError::Database)? + } else { + temps_entities::external_services::Entity::find() + .inner_join(temps_entities::backup_schedule_services::Entity) + .filter( + temps_entities::backup_schedule_services::Column::ScheduleId.eq(schedule.id), + ) + .all(self.db.as_ref()) + .await + .map_err(BackupError::Database)? + }; + + if external_services.is_empty() { + // Two reasons we could end up here: no DBs exist yet, or the + // operator picked "specific" mode and didn't attach anything. + // Log both with `target_all_services` so it's obvious which. + info!( + schedule_id = schedule.id, + schedule_name = %schedule.name, + target_all_services = schedule.target_all_services, + "enqueue_scheduled_run: no external services in scope; fan-out will be control-plane only", + ); + } // Resolve engine keys outside the transaction (async Docker probes). let mut resolved_services: Vec<(temps_entities::external_services::Model, &'static str)> = @@ -4699,62 +4901,71 @@ RETURNING id let mut jobs: Vec = Vec::new(); - // ── Step 4: control-plane backup ────────────────────────────────────── - - let cp_uuid = Uuid::new_v4().to_string(); - let cp_backup = temps_entities::backups::ActiveModel { - id: sea_orm::NotSet, - name: Set(format!("Backup {}", cp_uuid)), - backup_id: Set(cp_uuid.clone()), - schedule_id: Set(Some(schedule.id)), - schedule_run_id: Set(Some(run_id)), - backup_type: Set(schedule.backup_type.clone()), - state: Set("pending".to_string()), - started_at: Set(now), - finished_at: Set(None), - s3_source_id: Set(schedule.s3_source_id), - s3_location: Set(String::new()), - compression_type: Set("gzip".to_string()), - created_by: Set(0), - tags: Set("[]".to_string()), - size_bytes: Set(None), - file_count: Set(None), - error_message: Set(None), - expires_at: Set(None), - checksum: Set(None), - metadata: Set(serde_json::json!({ - "engine": "control_plane", - "async_runner": true, - "scheduled": triggered_by == TriggerSource::Cron, - "schedule_id": schedule.id, - "run_id": run_id, - "timestamp": now.to_rfc3339(), - }) - .to_string()), - }; - - let cp_backup_row = cp_backup.insert(&txn).await?; - // Defer queue publishes until after txn.commit so the consumer // can't dispatch an engine against a backups row the txn might // still roll back. let mut deferred_messages: Vec = Vec::new(); - deferred_messages.push(temps_core::BackupRequestedJob { - backup_id: cp_backup_row.id, - engine: "control_plane".to_string(), - params: serde_json::json!({ - "s3_source_id": schedule.s3_source_id, - "schedule_id": schedule.id, - "run_id": run_id, - }), - max_runtime_secs: schedule.max_runtime_secs.unwrap_or(4 * 60 * 60), - }); - jobs.push(EnqueuedJob { - backup_id: cp_backup_row.id, - job_id: cp_backup_row.id as i64, - engine: "control_plane".to_string(), - target_service_id: None, - }); + + // ── Step 4: control-plane backup (skipped when the schedule + // ── opts out of control-plane coverage). ───────────────────── + if schedule.include_control_plane { + let cp_uuid = Uuid::new_v4().to_string(); + let cp_backup = temps_entities::backups::ActiveModel { + id: sea_orm::NotSet, + name: Set(format!("Backup {}", cp_uuid)), + backup_id: Set(cp_uuid.clone()), + schedule_id: Set(Some(schedule.id)), + schedule_run_id: Set(Some(run_id)), + backup_type: Set(schedule.backup_type.clone()), + state: Set("pending".to_string()), + started_at: Set(now), + finished_at: Set(None), + s3_source_id: Set(schedule.s3_source_id), + s3_location: Set(String::new()), + compression_type: Set("gzip".to_string()), + created_by: Set(0), + tags: Set("[]".to_string()), + size_bytes: Set(None), + file_count: Set(None), + error_message: Set(None), + expires_at: Set(None), + checksum: Set(None), + metadata: Set(serde_json::json!({ + "engine": "control_plane", + "async_runner": true, + "scheduled": triggered_by == TriggerSource::Cron, + "schedule_id": schedule.id, + "run_id": run_id, + "timestamp": now.to_rfc3339(), + }) + .to_string()), + }; + + let cp_backup_row = cp_backup.insert(&txn).await?; + + deferred_messages.push(temps_core::BackupRequestedJob { + backup_id: cp_backup_row.id, + engine: "control_plane".to_string(), + params: serde_json::json!({ + "s3_source_id": schedule.s3_source_id, + "schedule_id": schedule.id, + "run_id": run_id, + }), + max_runtime_secs: schedule.max_runtime_secs.unwrap_or(4 * 60 * 60), + }); + jobs.push(EnqueuedJob { + backup_id: cp_backup_row.id, + job_id: cp_backup_row.id as i64, + engine: "control_plane".to_string(), + target_service_id: None, + }); + } else { + info!( + schedule_id = schedule.id, + run_id, + "enqueue_scheduled_run: include_control_plane=false, skipping control-plane backup", + ); + } // ── Step 5: external service backups ────────────────────────────────── @@ -6443,11 +6654,65 @@ ORDER BY esb.id ASC active.tags = Set(tags_json); changed_fields.push("tags"); } + if let Some(target_all) = request.target_all_services { + active.target_all_services = Set(target_all); + changed_fields.push("target_all_services"); + } + if let Some(include_cp) = request.include_control_plane { + active.include_control_plane = Set(include_cp); + changed_fields.push("include_control_plane"); + } + + // Pre-flight: figure out what state the schedule would be in after + // the update. If the operator is moving toward "nothing to back up," + // reject before we commit so the run history doesn't fill up with + // no-op runs. + let final_target_all = request + .target_all_services + .unwrap_or(existing.target_all_services); + let final_include_cp = request + .include_control_plane + .unwrap_or(existing.include_control_plane); + if !final_target_all && !final_include_cp { + use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter}; + let attached_count = temps_entities::backup_schedule_services::Entity::find() + .filter(temps_entities::backup_schedule_services::Column::ScheduleId.eq(id)) + .count(self.db.as_ref()) + .await + .map_err(BackupError::Database)?; + if attached_count == 0 { + return Err(BackupError::Validation( + "Schedule would have nothing to back up: \ + include_control_plane=false, target_all_services=false, \ + and no services attached. Attach at least one service \ + or re-enable one of the broader flags." + .to_string(), + )); + } + } active.updated_at = Set(Utc::now()); let updated = active.update(self.db.as_ref()).await?; + // When the caller flipped target_all_services to true, clear any + // stale explicit-membership rows. The user's choice ("clear it") + // means "all means all" — no hidden saved list to surface later if + // they flip back to specific. + if matches!(request.target_all_services, Some(true)) { + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; + let deleted = temps_entities::backup_schedule_services::Entity::delete_many() + .filter(temps_entities::backup_schedule_services::Column::ScheduleId.eq(id)) + .exec(self.db.as_ref()) + .await + .map_err(BackupError::Database)?; + info!( + schedule_id = id, + rows_deleted = deleted.rows_affected, + "Cleared explicit service memberships after flipping target_all_services=true", + ); + } + info!( schedule_id = id, fields = ?changed_fields, @@ -6618,6 +6883,8 @@ mod tests { description: None, tags: "[]".to_string(), max_runtime_secs: None, + target_all_services: true, + include_control_plane: true, } } @@ -7241,6 +7508,8 @@ mod tests { description: Some("Test backup schedule".to_string()), tags: vec![], max_runtime_secs: None, + target_all_services: None, + include_control_plane: None, }; let schedule = backup_service @@ -8134,6 +8403,8 @@ mod tests { max_runtime_secs: None, enabled: None, tags: None, + target_all_services: None, + include_control_plane: None, }; let result = svc.update_backup_schedule(1, request).await; @@ -8186,6 +8457,8 @@ mod tests { max_runtime_secs: None, enabled: None, tags: None, + target_all_services: None, + include_control_plane: None, }; let result = svc.update_backup_schedule(1, request).await; @@ -8235,6 +8508,8 @@ mod tests { max_runtime_secs: None, enabled: None, tags: None, + target_all_services: None, + include_control_plane: None, }; let result = svc.update_backup_schedule(1, request).await; @@ -8277,6 +8552,8 @@ mod tests { max_runtime_secs: None, enabled: None, tags: None, + target_all_services: None, + include_control_plane: None, }; let result = svc.update_backup_schedule(999, request).await; @@ -8644,4 +8921,728 @@ mod tests { "expected BackupError::NotFound for unknown parent backup" ); } + + // ── backup_schedule_services membership ────────────────────────────── + // + // These tests pin the contract of the attach/detach/list helpers. They + // need a `BackupService`, which in turn requires an + // `ExternalServiceManager`, which constructs a Docker client at build + // time. We early-return when Docker is unavailable so the suite stays + // green in CI environments without a daemon. + // + // The point of these is the *resolution* behaviour, not the SQL — the + // join query itself is exercised by the integration test. + + fn skip_if_no_docker() -> bool { + match bollard::Docker::connect_with_local_defaults() { + Ok(d) => { + // A `ping` would be more accurate but is async; the + // synchronous build is enough to keep tests green when the + // daemon socket is missing entirely. + drop(d); + false + } + Err(_) => { + println!("Docker not available, skipping test"); + true + } + } + } + + fn build_service_for_mock(db: Arc) -> Result { + if skip_if_no_docker() { + return Err(()); + } + Ok(BackupService::new( + db.clone(), + create_mock_external_service_manager(db), + create_mock_notification_service(), + create_mock_config_service(), + Arc::new(EncryptionService::new("test_encryption_key_1234567890ab").unwrap()), + )) + } + + #[tokio::test] + async fn attach_services_rejects_unknown_schedule() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + // get_backup_schedule -> find_by_id returns empty + .append_query_results(vec![Vec::::new()]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + let err = svc + .attach_services_to_schedule(42, &[1, 2, 3]) + .await + .expect_err("missing schedule should error"); + assert!( + matches!(err, BackupError::NotFound { .. }), + "expected NotFound, got {:?}", + err + ); + } + + #[tokio::test] + async fn attach_services_noop_on_empty_input() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + // schedule lookup succeeds + .append_query_results(vec![vec![make_test_schedule(7, 1)]]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + // Empty list must short-circuit before any further query is issued — + // we only queued one query result (the schedule lookup). + let inserted = svc + .attach_services_to_schedule(7, &[]) + .await + .expect("empty attach should succeed"); + assert_eq!(inserted, 0); + } + + // Note: validation-of-unknown-service-ids is covered by the integration + // test (`integration_attach_list_detach_round_trip`) because mocking + // Sea-ORM's `.count()` requires a query-result shape that `MockDatabase` + // does not accept generically. The integration test exercises the same + // code path against a real Postgres. + + #[tokio::test] + async fn detach_service_returns_false_when_no_row() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results(vec![MockExecResult { + last_insert_id: 0, + rows_affected: 0, + }]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + let removed = svc + .detach_service_from_schedule(1, 2) + .await + .expect("detach should be idempotent"); + assert!(!removed, "no row → returns false"); + } + + #[tokio::test] + async fn detach_service_returns_true_when_removed() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results(vec![MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + let removed = svc + .detach_service_from_schedule(1, 2) + .await + .expect("detach should succeed"); + assert!(removed); + } + + #[tokio::test] + async fn list_services_for_unknown_schedule_returns_not_found() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![Vec::::new()]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + let err = svc + .list_services_for_schedule(404) + .await + .expect_err("missing schedule must error"); + assert!(matches!(err, BackupError::NotFound { .. })); + } + + #[tokio::test] + async fn list_schedules_for_unknown_service_returns_not_found() { + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + // external_services::Entity::find_by_id → empty + .append_query_results(vec![Vec::::new()]) + .into_connection(), + ); + let Ok(svc) = build_service_for_mock(db) else { + return; + }; + + let err = svc + .list_schedules_for_service(123) + .await + .expect_err("missing service must error"); + assert!(matches!(err, BackupError::NotFound { .. })); + } + + /// Integration test: round-trip attach → list → detach against a real + /// Postgres backed by `TestDatabase::with_migrations`. Verifies the + /// migration creates the join table correctly, the FKs cascade on + /// service-and-schedule delete, and the resolver join returns the right + /// rows. Skips gracefully when Docker (and therefore the test Postgres) + /// is unavailable. + #[tokio::test] + async fn integration_attach_list_detach_round_trip() { + if bollard::Docker::connect_with_local_defaults().is_err() { + println!("Docker not available, skipping test"); + return; + } + use sea_orm::ActiveValue::Set; + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; + use temps_database::test_utils::TestDatabase; + + let test_db = match TestDatabase::with_migrations().await { + Ok(d) => d, + Err(e) => { + println!("TestDatabase unavailable, skipping: {e}"); + return; + } + }; + let db = test_db.db.clone(); + + // Seed an S3 source (FK target for schedule). + let s3_source = temps_entities::s3_sources::ActiveModel { + id: sea_orm::NotSet, + name: Set("integration-source".to_string()), + bucket_name: Set("test-bucket".to_string()), + bucket_path: Set("/".to_string()), + access_key_id: Set("".to_string()), + secret_key: Set("".to_string()), + region: Set("us-east-1".to_string()), + endpoint: Set(None), + force_path_style: Set(Some(true)), + is_default: Set(true), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + } + .insert(db.as_ref()) + .await + .expect("insert s3 source"); + + // Seed a schedule. Use 'specific' mode so the explicit-membership + // path is exercised by this test (the integration test for the + // 'all' branch lives in `integration_flip_to_all_clears_membership`). + let schedule = temps_entities::backup_schedules::ActiveModel { + id: sea_orm::NotSet, + name: Set("integration-schedule".to_string()), + backup_type: Set("full".to_string()), + retention_period: Set(7), + s3_source_id: Set(s3_source.id), + schedule_expression: Set("0 0 2 * * *".to_string()), + enabled: Set(true), + last_run: Set(None), + next_run: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + description: Set(None), + tags: Set("[]".to_string()), + max_runtime_secs: Set(None), + target_all_services: Set(false), + include_control_plane: Set(true), + } + .insert(db.as_ref()) + .await + .expect("insert schedule"); + + // Seed two external services. + let mk_svc = |name: &str, svc_type: &str| temps_entities::external_services::ActiveModel { + id: sea_orm::NotSet, + name: Set(name.to_string()), + service_type: Set(svc_type.to_string()), + version: Set(Some("17".to_string())), + status: Set("running".to_string()), + slug: Set(Some(name.to_string())), + config: Set(None), + node_id: Set(None), + topology: Set("standalone".to_string()), + error_message: Set(None), + health_status: Set(None), + last_health_check_at: Set(None), + last_health_error: Set(None), + consecutive_health_failures: Set(0), + health_metadata: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + }; + let pg = mk_svc("pg-prod", "postgres") + .insert(db.as_ref()) + .await + .expect("insert pg service"); + let redis = mk_svc("redis-prod", "redis") + .insert(db.as_ref()) + .await + .expect("insert redis service"); + + // Build a service. We can't use build_service_for_mock because we + // want the *real* DB, not a mock. The Docker handle is required by + // ExternalServiceManager but unused by these methods. + let svc = BackupService::new( + db.clone(), + create_mock_external_service_manager(db.clone()), + create_mock_notification_service(), + create_mock_config_service(), + Arc::new(EncryptionService::new("test_encryption_key_1234567890ab").unwrap()), + ); + + // 1) Attach both services. + let inserted = svc + .attach_services_to_schedule(schedule.id, &[pg.id, redis.id]) + .await + .expect("attach succeeds"); + assert_eq!(inserted, 2, "both rows inserted"); + + // 2) Re-attaching is idempotent (ON CONFLICT DO NOTHING). + let inserted_again = svc + .attach_services_to_schedule(schedule.id, &[pg.id, redis.id]) + .await + .expect("re-attach succeeds"); + assert_eq!(inserted_again, 0, "no new rows on duplicate attach"); + + // 3) list_services_for_schedule returns both, ordered by name. + let listed = svc + .list_services_for_schedule(schedule.id) + .await + .expect("list services"); + assert_eq!(listed.len(), 2); + // Sorted by name: pg-prod < redis-prod + assert_eq!(listed[0].name, "pg-prod"); + assert_eq!(listed[1].name, "redis-prod"); + + // 4) list_schedules_for_service returns the schedule for each. + let pg_schedules = svc + .list_schedules_for_service(pg.id) + .await + .expect("list schedules for pg"); + assert_eq!(pg_schedules.len(), 1); + assert_eq!(pg_schedules[0].id, schedule.id); + + // 5) Detach one service. + let removed = svc + .detach_service_from_schedule(schedule.id, pg.id) + .await + .expect("detach succeeds"); + assert!(removed); + let listed = svc + .list_services_for_schedule(schedule.id) + .await + .expect("list after detach"); + assert_eq!(listed.len(), 1); + assert_eq!(listed[0].name, "redis-prod"); + + // 6) Detach again is idempotent (returns false, no error). + let removed_again = svc + .detach_service_from_schedule(schedule.id, pg.id) + .await + .expect("idempotent detach"); + assert!(!removed_again); + + // 7) Cascade: deleting the schedule removes all membership rows. + temps_entities::backup_schedules::Entity::delete_by_id(schedule.id) + .exec(db.as_ref()) + .await + .expect("delete schedule"); + let leftover = temps_entities::backup_schedule_services::Entity::find() + .filter(temps_entities::backup_schedule_services::Column::ScheduleId.eq(schedule.id)) + .all(db.as_ref()) + .await + .expect("count leftover"); + assert!( + leftover.is_empty(), + "schedule delete must cascade to membership" + ); + } + + /// Integration test: when `target_all_services = true`, flipping a + /// schedule's mode via `update_backup_schedule` clears all explicit + /// membership rows (clean-slate behaviour). When set back to false, + /// the rows are not magically restored — the user has to attach + /// again. Skips gracefully when Docker / test Postgres are absent. + #[tokio::test] + async fn integration_flip_to_all_clears_membership() { + if bollard::Docker::connect_with_local_defaults().is_err() { + println!("Docker not available, skipping test"); + return; + } + use sea_orm::ActiveValue::Set; + use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; + use temps_database::test_utils::TestDatabase; + + let test_db = match TestDatabase::with_migrations().await { + Ok(d) => d, + Err(e) => { + println!("TestDatabase unavailable, skipping: {e}"); + return; + } + }; + let db = test_db.db.clone(); + + // Seed S3 source + schedule (start in 'specific' mode so we have + // membership rows to clear). + let s3 = temps_entities::s3_sources::ActiveModel { + id: sea_orm::NotSet, + name: Set("flip-source".to_string()), + bucket_name: Set("b".to_string()), + bucket_path: Set("/".to_string()), + access_key_id: Set("".to_string()), + secret_key: Set("".to_string()), + region: Set("us-east-1".to_string()), + endpoint: Set(None), + force_path_style: Set(Some(true)), + is_default: Set(true), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + } + .insert(db.as_ref()) + .await + .expect("insert s3 source"); + + let schedule = temps_entities::backup_schedules::ActiveModel { + id: sea_orm::NotSet, + name: Set("flip-schedule".to_string()), + backup_type: Set("full".to_string()), + retention_period: Set(7), + s3_source_id: Set(s3.id), + schedule_expression: Set("0 0 2 * * *".to_string()), + enabled: Set(true), + last_run: Set(None), + next_run: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + description: Set(None), + tags: Set("[]".to_string()), + max_runtime_secs: Set(None), + // Start as specific so we can attach rows. + target_all_services: Set(false), + include_control_plane: Set(true), + } + .insert(db.as_ref()) + .await + .expect("insert schedule"); + + let svc_a = temps_entities::external_services::ActiveModel { + id: sea_orm::NotSet, + name: Set("svc-a".to_string()), + service_type: Set("postgres".to_string()), + version: Set(Some("17".to_string())), + status: Set("running".to_string()), + slug: Set(Some("svc-a".to_string())), + config: Set(None), + node_id: Set(None), + topology: Set("standalone".to_string()), + error_message: Set(None), + health_status: Set(None), + last_health_check_at: Set(None), + last_health_error: Set(None), + consecutive_health_failures: Set(0), + health_metadata: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + } + .insert(db.as_ref()) + .await + .expect("insert svc-a"); + + let svc = BackupService::new( + db.clone(), + create_mock_external_service_manager(db.clone()), + create_mock_notification_service(), + create_mock_config_service(), + Arc::new(EncryptionService::new("test_encryption_key_1234567890ab").unwrap()), + ); + + // Attach svc-a to the specific schedule. + svc.attach_services_to_schedule(schedule.id, &[svc_a.id]) + .await + .expect("attach"); + + let listed = svc + .list_services_for_schedule(schedule.id) + .await + .expect("list pre-flip"); + assert_eq!(listed.len(), 1, "precondition: one service attached"); + + // Flip to target_all_services = true via the service-layer update + // (mirrors what the handler does on PATCH). + svc.update_backup_schedule( + schedule.id, + crate::handlers::backup_handler::UpdateBackupScheduleRequest { + name: None, + description: None, + schedule_expression: None, + retention_period: None, + max_runtime_secs: None, + enabled: None, + tags: None, + target_all_services: Some(true), + include_control_plane: None, + }, + ) + .await + .expect("update succeeds"); + + // Membership table must now be empty for this schedule. + let after = temps_entities::backup_schedule_services::Entity::find() + .filter(temps_entities::backup_schedule_services::Column::ScheduleId.eq(schedule.id)) + .all(db.as_ref()) + .await + .expect("count after flip"); + assert!( + after.is_empty(), + "flipping to target_all_services=true must clear membership rows" + ); + + // Flip back to specific — list must stay empty (we cleared it). + svc.update_backup_schedule( + schedule.id, + crate::handlers::backup_handler::UpdateBackupScheduleRequest { + name: None, + description: None, + schedule_expression: None, + retention_period: None, + max_runtime_secs: None, + enabled: None, + tags: None, + target_all_services: Some(false), + include_control_plane: None, + }, + ) + .await + .expect("update back to specific"); + + let after_specific = svc + .list_services_for_schedule(schedule.id) + .await + .expect("list after flip-back"); + assert!( + after_specific.is_empty(), + "flipping back to specific must not magically restore membership" + ); + } + + /// Unit test (no DB needed): create_backup_schedule rejects a request + /// that would produce a no-op schedule (include_control_plane=false + /// AND target_all_services=false). The validation runs before any + /// DB call, so we don't even need a working Docker daemon for this. + #[tokio::test] + async fn create_rejects_empty_fan_out() { + // Build a service with a mock DB. We never reach the DB because + // validation fires first. + if skip_if_no_docker() { + return; + } + let db = Arc::new( + MockDatabase::new(DatabaseBackend::Postgres) + // resolve_s3_source_id: caller passed Some(1) so this query + // (find_by_id) is the next thing the service does. + .append_query_results(vec![vec![s3_sources::Model { + id: 1, + name: "s".to_string(), + bucket_name: "b".to_string(), + bucket_path: "/".to_string(), + access_key_id: "".to_string(), + secret_key: "".to_string(), + region: "us-east-1".to_string(), + endpoint: None, + force_path_style: Some(true), + is_default: true, + created_at: Utc::now(), + updated_at: Utc::now(), + }]]) + .into_connection(), + ); + let svc = BackupService::new( + db.clone(), + create_mock_external_service_manager(db), + create_mock_notification_service(), + create_mock_config_service(), + Arc::new(EncryptionService::new("test_encryption_key_1234567890ab").unwrap()), + ); + + let request = CreateBackupScheduleRequest { + name: "bad".to_string(), + backup_type: "full".to_string(), + retention_period: 7, + s3_source_id: Some(1), + schedule_expression: "0 0 2 * * *".to_string(), + enabled: true, + description: None, + tags: vec![], + max_runtime_secs: None, + target_all_services: Some(false), + include_control_plane: Some(false), + }; + + let err = svc + .create_backup_schedule(request) + .await + .expect_err("empty fan-out must be rejected"); + assert!( + matches!(err, BackupError::Validation(ref msg) if msg.contains("control plane")), + "expected Validation error mentioning control plane, got {:?}", + err + ); + } + + /// Integration test: when `include_control_plane = false` and a single + /// service is attached, the fan-out produces exactly one backup row + /// (no control-plane row alongside it). This is the scenario from + /// the user report where picking one Postgres still produced a + /// `control_plane` backup as a sidecar. + #[tokio::test] + async fn integration_fan_out_skips_control_plane_when_flag_off() { + if bollard::Docker::connect_with_local_defaults().is_err() { + println!("Docker not available, skipping test"); + return; + } + use sea_orm::ActiveValue::Set; + use sea_orm::{ColumnTrait, EntityTrait}; + use temps_database::test_utils::TestDatabase; + + let test_db = match TestDatabase::with_migrations().await { + Ok(d) => d, + Err(e) => { + println!("TestDatabase unavailable, skipping: {e}"); + return; + } + }; + let db = test_db.db.clone(); + + let s3 = temps_entities::s3_sources::ActiveModel { + id: sea_orm::NotSet, + name: Set("cp-skip-source".to_string()), + bucket_name: Set("b".to_string()), + bucket_path: Set("/".to_string()), + access_key_id: Set("".to_string()), + secret_key: Set("".to_string()), + region: Set("us-east-1".to_string()), + endpoint: Set(None), + force_path_style: Set(Some(true)), + is_default: Set(true), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + } + .insert(db.as_ref()) + .await + .expect("insert s3 source"); + + // Schedule: specific mode, no control plane. + let schedule = temps_entities::backup_schedules::ActiveModel { + id: sea_orm::NotSet, + name: Set("cp-skip-schedule".to_string()), + backup_type: Set("full".to_string()), + retention_period: Set(7), + s3_source_id: Set(s3.id), + schedule_expression: Set("0 0 2 * * *".to_string()), + enabled: Set(true), + last_run: Set(None), + next_run: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + description: Set(None), + tags: Set("[]".to_string()), + max_runtime_secs: Set(None), + target_all_services: Set(false), + include_control_plane: Set(false), + } + .insert(db.as_ref()) + .await + .expect("insert schedule"); + + let svc_pg = temps_entities::external_services::ActiveModel { + id: sea_orm::NotSet, + name: Set("pg-only".to_string()), + service_type: Set("postgres".to_string()), + version: Set(Some("17".to_string())), + status: Set("running".to_string()), + slug: Set(Some("pg-only".to_string())), + config: Set(None), + node_id: Set(None), + topology: Set("standalone".to_string()), + error_message: Set(None), + health_status: Set(None), + last_health_check_at: Set(None), + last_health_error: Set(None), + consecutive_health_failures: Set(0), + health_metadata: Set(None), + created_at: Set(chrono::Utc::now()), + updated_at: Set(chrono::Utc::now()), + } + .insert(db.as_ref()) + .await + .expect("insert pg"); + + let svc = BackupService::new( + db.clone(), + create_mock_external_service_manager(db.clone()), + create_mock_notification_service(), + create_mock_config_service(), + Arc::new(EncryptionService::new("test_encryption_key_1234567890ab").unwrap()), + ); + + svc.attach_services_to_schedule(schedule.id, &[svc_pg.id]) + .await + .expect("attach"); + + // Sanity: post-attach, the schedule is well-formed (control-plane + // off + specific mode + 1 attached service). + let after_attach = svc + .list_services_for_schedule(schedule.id) + .await + .expect("list"); + assert_eq!(after_attach.len(), 1); + + // Flip-empty test: updating to include_control_plane=false with + // *no* attached services (we'll detach first) must fail. + svc.detach_service_from_schedule(schedule.id, svc_pg.id) + .await + .expect("detach"); + let err = svc + .update_backup_schedule( + schedule.id, + crate::handlers::backup_handler::UpdateBackupScheduleRequest { + name: None, + description: None, + schedule_expression: None, + retention_period: None, + max_runtime_secs: None, + enabled: None, + tags: None, + target_all_services: None, + include_control_plane: Some(false), + }, + ) + .await + .expect_err("empty fan-out must be rejected"); + assert!( + matches!(err, BackupError::Validation(ref msg) if msg.contains("nothing to back up")), + "expected Validation error, got {:?}", + err + ); + + // Cleanup: schedule has no children, so the cascade can drop it. + let _ = temps_entities::backup_schedules::Entity::delete_by_id(schedule.id) + .exec(db.as_ref()) + .await; + let _ = temps_entities::external_services::Entity::delete_by_id(svc_pg.id) + .exec(db.as_ref()) + .await; + // Silence unused warning on the QueryFilter / ColumnTrait imports. + let _ = temps_entities::backup_schedule_services::Column::ScheduleId.eq(0); + } } diff --git a/crates/temps-backup/src/services/mod.rs b/crates/temps-backup/src/services/mod.rs index 140a67c9..bd9f5212 100644 --- a/crates/temps-backup/src/services/mod.rs +++ b/crates/temps-backup/src/services/mod.rs @@ -3,7 +3,10 @@ mod backup; mod notifier; mod reconcile; mod restore; -mod s3_lifecycle; +// `pub(crate)` so the upload path in `engines::v2_common::apply_object_tags` +// can reuse `is_unsupported_error` to decide whether a tagging failure is +// "this provider doesn't support tags" (warn + continue) vs a real error. +pub(crate) mod s3_lifecycle; pub use alerts::{sweep_backup_alerts, SweepStats, OVERDUE_GRACE}; pub use backup::{ BackupError, BackupService, BackupTriggerParams, ChildBackupEntry, EnqueuedJob, diff --git a/crates/temps-backup/src/services/s3_lifecycle.rs b/crates/temps-backup/src/services/s3_lifecycle.rs index 0b7a86eb..822e3e46 100644 --- a/crates/temps-backup/src/services/s3_lifecycle.rs +++ b/crates/temps-backup/src/services/s3_lifecycle.rs @@ -272,7 +272,12 @@ async fn clear_temps_rules( /// these as generic service errors; the response body text is the only /// signal. The strings here cover AWS, MinIO, OVH, R2, and B2 rejections /// observed in practice. -fn is_unsupported_error(msg: &str) -> bool { +/// +/// Re-exported as `pub` so the upload path (`apply_object_tags`) can use +/// the same matching to decide whether a tag-write failure is "this +/// provider can't" (warn + continue) vs "the upload is genuinely broken" +/// (fail the backup). +pub fn is_unsupported_error(msg: &str) -> bool { let m = msg.to_lowercase(); m.contains("notimplemented") || m.contains("not implemented") @@ -309,6 +314,8 @@ mod tests { description: None, tags: "{}".to_string(), max_runtime_secs: None, + target_all_services: true, + include_control_plane: true, } } @@ -356,6 +363,36 @@ mod tests { assert!(!is_unsupported_error("NoSuchBucket")); } + /// Regression: R2 returns this exact shape when `PutObjectTagging` + /// is called. The upload-path uses `is_unsupported_error` on the + /// rendered `describe_sdk_error` string to decide whether to fail + /// the backup or warn + continue. + #[test] + fn is_unsupported_error_matches_r2_put_object_tagging() { + let r2_describe = "put_object_tagging on s3://bucket/key failed | HTTP 501 \ + | code=NotImplemented | message=PutObjectTagging not implemented \ + | body=\ + NotImplementedPutObjectTagging not implemented\ + "; + assert!( + is_unsupported_error(r2_describe), + "must recognise the R2 PutObjectTagging 501 shape" + ); + } + + /// Regression: R2 also returns the same `NotImplemented` family when + /// the `x-amz-tagging` header is passed on a put/create-multipart + /// upload. The upload path no longer sends that header, but if a + /// future change re-introduces it the `is_unsupported_error` matcher + /// must still classify it correctly. + #[test] + fn is_unsupported_error_matches_r2_x_amz_tagging() { + let r2_describe = "create_multipart_upload failed | HTTP 501 \ + | code=NotImplemented | message=Header 'x-amz-tagging' with value \ + 'temps-managed=true&temps-retention-days=7' not implemented"; + assert!(is_unsupported_error(r2_describe)); + } + /// Build an S3 client pointed at an arbitrary endpoint with hardcoded /// credentials. Mirrors `engines::v2_common::build_s3_client` but /// bypasses the encryption layer so testcontainer fixtures stay terse. diff --git a/crates/temps-cli/Cargo.toml b/crates/temps-cli/Cargo.toml index 9a4303a5..6eb298a5 100644 --- a/crates/temps-cli/Cargo.toml +++ b/crates/temps-cli/Cargo.toml @@ -56,7 +56,6 @@ temps-import = { path = "../temps-import" } temps-infra = { path = "../temps-infra" } temps-log-aggregator = { path = "../temps-log-aggregator" } temps-logs = { path = "../temps-logs" } -# temps-mcp = { path = "../temps-mcp" } temps-migrations = { path = "../temps-migrations" } temps-monitoring = { path = "../temps-monitoring" } temps-notifications = { path = "../temps-notifications" } @@ -75,7 +74,6 @@ temps-wireguard = { path = "../temps-wireguard" } tokio-util = { workspace = true } # CLI and runtime dependencies - reference from crates workspace -check-if-email-exists = "0.11" clap = "4.4" colored = "2.0" chrono = { workspace = true } diff --git a/crates/temps-cli/src/commands/serve/mod.rs b/crates/temps-cli/src/commands/serve/mod.rs index de556273..55f6b4ce 100644 --- a/crates/temps-cli/src/commands/serve/mod.rs +++ b/crates/temps-cli/src/commands/serve/mod.rs @@ -82,10 +82,9 @@ impl ServeCommand { self, extra_plugins: Vec>, ) -> anyhow::Result<()> { - // Install the rustls crypto provider once at startup. Both temps-domains - // and check-if-email-exists try to install it themselves — calling it here - // first satisfies the library's internal Once guard and prevents panics. - check_if_email_exists::initialize_crypto_provider(); + // Install the rustls crypto provider once at startup, before any + // dependency (e.g. temps-domains) constructs a rustls client. + crate::install_crypto_provider(); // Set screenshot provider from CLI flag (takes precedence over env var) // This allows: temps serve --screenshot-provider=noop diff --git a/crates/temps-cli/src/lib.rs b/crates/temps-cli/src/lib.rs index 0416b06c..81b8573f 100644 --- a/crates/temps-cli/src/lib.rs +++ b/crates/temps-cli/src/lib.rs @@ -111,7 +111,6 @@ pub fn install_tracing(log_level: &str, log_format: &str) { temps_notifications={level},\ temps_infra={level},\ temps_geo={level},\ - temps_mcp={level},\ temps_entities={level},\ temps_database={level},\ temps_migrations={level},\ @@ -211,11 +210,22 @@ pub fn dispatch( } } +/// Install the process-wide rustls crypto provider exactly once. +/// +/// Several dependencies (e.g. `temps-domains`) construct rustls clients and +/// expect a default `CryptoProvider` to be present. `install_default` +/// returns `Err` if one is already installed, which is the normal outcome on +/// the second and later calls — so the error is intentionally ignored, +/// giving the same idempotent behaviour the old library helper provided. +pub fn install_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + /// Convenience entrypoint that parses, installs tracing, and dispatches. /// Used by both the OSS `temps` binary (`extra_plugins = vec![]`) and any /// EE-bundled binary that wraps the same CLI surface. pub fn run(extra_plugins: Vec>) -> anyhow::Result<()> { - check_if_email_exists::initialize_crypto_provider(); + install_crypto_provider(); let cli = Cli::parse(); install_tracing(&cli.log_level, &cli.log_format); dispatch(cli, extra_plugins) diff --git a/crates/temps-deployments/src/jobs/mark_deployment_complete.rs b/crates/temps-deployments/src/jobs/mark_deployment_complete.rs index 0fc84985..c86c00e0 100644 --- a/crates/temps-deployments/src/jobs/mark_deployment_complete.rs +++ b/crates/temps-deployments/src/jobs/mark_deployment_complete.rs @@ -482,6 +482,39 @@ impl MarkDeploymentCompleteJob { ))); } + // ── Phase 0: Persist routing inputs before flipping the route table ── + // + // load_routes() reads `deployments.static_dir_location` (and + // `image_name`) to build the backend for an environment. Those fields + // are otherwise only written in Phase 3's `active_deployment.update()`, + // which runs AFTER current_deployment_id is flipped. For static + // deployments that means the PG NOTIFY fires while static_dir_location + // is still NULL, so the proxy builds a route with no static directory + // and the folder isn't served until a later, unrelated route reload. + // + // Write the routing-relevant fields to the deployment row FIRST so the + // route table sees a consistent record the moment the NOTIFY fires. + { + let mut routing_inputs = deployments::ActiveModel { + id: sea_orm::ActiveValue::Unchanged(self.deployment_id), + ..Default::default() + }; + if let sea_orm::ActiveValue::Set(image) = &active_deployment.image_name { + routing_inputs.image_name = Set(image.clone()); + } + if let sea_orm::ActiveValue::Set(static_dir) = &active_deployment.static_dir_location { + routing_inputs.static_dir_location = Set(static_dir.clone()); + } + if routing_inputs.is_changed() { + routing_inputs.update(self.db.as_ref()).await.map_err(|e| { + WorkflowError::JobExecutionFailed(format!( + "Failed to persist deployment routing inputs: {}", + e + )) + })?; + } + } + // ── Phase 1: Switch route table to the new deployment ──────────── // // Subscribe to the queue BEFORE updating current_deployment_id so we diff --git a/crates/temps-dns-resolver/Cargo.toml b/crates/temps-dns-resolver/Cargo.toml index 936d9fe2..5dbf312f 100644 --- a/crates/temps-dns-resolver/Cargo.toml +++ b/crates/temps-dns-resolver/Cargo.toml @@ -12,13 +12,13 @@ homepage.workspace = true # Pinned to 0.25 to match the resolver versions used elsewhere in the # workspace (`temps-domains`). 0.26 has alpha-only client/proto bundles # at the time of writing. -hickory-server = { version = "0.25", default-features = false } -hickory-proto = { version = "0.25", default-features = false } +hickory-server = { version = "0.26", default-features = false } +hickory-proto = { version = "0.26", default-features = false } # Recursive client used to forward non-`temps.local` queries to upstream # public resolvers (Cloudflare/Google by default). Without this, app # containers using us as their first nameserver get NXDOMAIN for # everything outside our internal zone. -hickory-resolver = "0.25" +hickory-resolver = "0.26" # Async runtime tokio = { workspace = true } @@ -40,8 +40,7 @@ anyhow = { workspace = true } chrono = { workspace = true } [dev-dependencies] -hickory-resolver = "0.25" -hickory-client = "0.25" +hickory-resolver = "0.26" tokio-test = "0.4" wiremock = "0.6" tempfile = "3" diff --git a/crates/temps-dns-resolver/src/authority.rs b/crates/temps-dns-resolver/src/authority.rs index a931c0ca..2c35cc86 100644 --- a/crates/temps-dns-resolver/src/authority.rs +++ b/crates/temps-dns-resolver/src/authority.rs @@ -18,11 +18,12 @@ use std::sync::Arc; -use hickory_proto::op::{Header, MessageType, OpCode, ResponseCode}; +use hickory_proto::op::{Header, HeaderCounts, MessageType, Metadata, OpCode, ResponseCode}; use hickory_proto::rr::rdata::{A as RDataA, AAAA as RDataAAAA, CNAME as RDataCNAME}; use hickory_proto::rr::{Name, RData, Record, RecordType}; -use hickory_server::authority::MessageResponseBuilder; +use hickory_server::net::runtime::Time; use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; +use hickory_server::zone_handler::MessageResponseBuilder; use std::net::IpAddr; use std::str::FromStr; use tracing::{debug, trace, warn}; @@ -62,7 +63,7 @@ fn is_internal_zone(qname: &str) -> bool { #[async_trait::async_trait] impl RequestHandler for ZoneAuthority { - async fn handle_request( + async fn handle_request( &self, request: &Request, mut response_handle: R, @@ -76,12 +77,12 @@ impl RequestHandler for ZoneAuthority { }; // Only standard queries are supported. - if info.header.op_code() != OpCode::Query - || info.header.message_type() != MessageType::Query + if info.metadata.op_code != OpCode::Query + || info.metadata.message_type != MessageType::Query { trace!( - op = ?info.header.op_code(), - ty = ?info.header.message_type(), + op = ?info.metadata.op_code, + ty = ?info.metadata.message_type, "rejecting non-Query DNS message" ); return reply_error(request, &mut response_handle, ResponseCode::NotImp).await; @@ -139,7 +140,7 @@ impl RequestHandler for ZoneAuthority { if any_match { // NODATA: name exists, just not for this qtype. Reply // NoError with no answer rrs and the AA bit set. - return reply_nodata(request, &mut response_handle, info.header).await; + return reply_nodata(request, &mut response_handle, info.metadata).await; } // Genuine NXDOMAIN. return reply_error(request, &mut response_handle, ResponseCode::NXDomain).await; @@ -158,13 +159,13 @@ impl RequestHandler for ZoneAuthority { return reply_error(request, &mut response_handle, ResponseCode::ServFail).await; } - let mut header = Header::response_from_request(info.header); - header.set_authoritative(true); - header.set_response_code(ResponseCode::NoError); + let mut metadata = Metadata::response_from_request(info.metadata); + metadata.authoritative = true; + metadata.response_code = ResponseCode::NoError; let builder = MessageResponseBuilder::from_message_request(request); let resp = builder.build( - header, + metadata, answers.iter(), std::iter::empty::<&Record>(), std::iter::empty::<&Record>(), @@ -235,7 +236,8 @@ async fn reply_error( code: ResponseCode, ) -> ResponseInfo { let builder = MessageResponseBuilder::from_message_request(request); - let resp = builder.error_msg(request.header(), code); + // `Request` derefs to `Metadata`; `error_msg` wants `&Metadata`. + let resp = builder.error_msg(&request.metadata, code); match response_handle.send_response(resp).await { Ok(info) => info, Err(e) => { @@ -253,15 +255,15 @@ async fn reply_error( async fn reply_nodata( request: &Request, response_handle: &mut R, - request_header: &Header, + request_metadata: &Metadata, ) -> ResponseInfo { - let mut header = Header::response_from_request(request_header); - header.set_authoritative(true); - header.set_response_code(ResponseCode::NoError); + let mut metadata = Metadata::response_from_request(request_metadata); + metadata.authoritative = true; + metadata.response_code = ResponseCode::NoError; let builder = MessageResponseBuilder::from_message_request(request); let resp = builder.build( - header, + metadata, std::iter::empty::<&Record>(), std::iter::empty::<&Record>(), std::iter::empty::<&Record>(), @@ -277,9 +279,15 @@ async fn reply_nodata( } fn error_info(request: &Request, code: ResponseCode) -> ResponseInfo { - let mut header = Header::response_from_request(request.header()); - header.set_response_code(code); - header.into() + // `Request` derefs to `Metadata`; build a fresh response Header from it. + // `ResponseInfo` is constructed from a `Header` (Metadata + record counts). + let mut metadata = Metadata::response_from_request(&request.metadata); + metadata.response_code = code; + Header { + metadata, + counts: HeaderCounts::default(), + } + .into() } #[cfg(test)] @@ -328,8 +336,8 @@ mod tests { fn build_answer_emits_a_record() { let qname = Name::from_str("x.temps.local.").unwrap(); let answer = build_answer(&qname, &rec("A", "172.20.5.10")).unwrap(); - assert_eq!(answer.ttl(), 30); - match answer.data() { + assert_eq!(answer.ttl, 30); + match &answer.data { RData::A(RDataA(v4)) => assert_eq!(v4.to_string(), "172.20.5.10"), other => panic!("expected A, got {other:?}"), } @@ -345,7 +353,7 @@ mod tests { fn build_answer_emits_aaaa_record() { let qname = Name::from_str("x.temps.local.").unwrap(); let answer = build_answer(&qname, &rec("AAAA", "fd00::1")).unwrap(); - match answer.data() { + match &answer.data { RData::AAAA(RDataAAAA(v6)) => assert!(v6.to_string().contains("fd00")), other => panic!("expected AAAA, got {other:?}"), } diff --git a/crates/temps-dns-resolver/src/handle.rs b/crates/temps-dns-resolver/src/handle.rs index 8a6a999e..ee576276 100644 --- a/crates/temps-dns-resolver/src/handle.rs +++ b/crates/temps-dns-resolver/src/handle.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use std::time::Duration; -use hickory_server::ServerFuture; +use hickory_server::server::Server; use tokio::net::{TcpListener, UdpSocket}; use tokio::sync::Notify; use tokio::task::JoinHandle; @@ -71,7 +71,7 @@ impl ResolverHandle { if let Some(upstream) = upstream { authority = authority.with_upstream(upstream); } - let mut server = ServerFuture::new(authority); + let mut server = Server::new(authority); for addr in &config.listen_addrs { let udp = @@ -90,7 +90,9 @@ impl ResolverHandle { addr: *addr, source, })?; - server.register_listener(tcp, TCP_IDLE_TIMEOUT); + // 65535 = the maximum DNS-over-TCP message size (2-byte length + // prefix), so a single response never has to be split. + server.register_listener(tcp, TCP_IDLE_TIMEOUT, u16::MAX as usize); info!(%addr, "DNS resolver listening (UDP + TCP)"); } diff --git a/crates/temps-dns-resolver/src/upstream.rs b/crates/temps-dns-resolver/src/upstream.rs index 7323d7b6..bd0dc737 100644 --- a/crates/temps-dns-resolver/src/upstream.rs +++ b/crates/temps-dns-resolver/src/upstream.rs @@ -23,17 +23,16 @@ use std::net::SocketAddr; use std::time::Duration; -use hickory_proto::op::{Header, ResponseCode}; +use hickory_proto::op::{Header, HeaderCounts, Metadata, ResponseCode}; use hickory_proto::rr::{Name, Record}; use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts}; -use hickory_resolver::name_server::TokioConnectionProvider; -use hickory_resolver::proto::xfer::Protocol; +use hickory_resolver::net::runtime::TokioRuntimeProvider; use hickory_resolver::Resolver; -use hickory_server::authority::MessageResponseBuilder; use hickory_server::server::{Request, ResponseHandler, ResponseInfo}; +use hickory_server::zone_handler::MessageResponseBuilder; use tracing::{trace, warn}; -type TokioResolver = Resolver; +type TokioResolver = Resolver; /// Forwards queries that fall outside the internal `temps.local` zone /// to the upstream pool. Construct once per agent process and share via @@ -52,12 +51,13 @@ impl UpstreamResolver { return None; } - let mut config = ResolverConfig::new(); + let mut config = ResolverConfig::default(); for addr in upstreams { - config.add_name_server(NameServerConfig::new(*addr, Protocol::Udp)); - // TCP fallback for responses too large for UDP (e.g. some - // TXT and DNSSEC answers). Same socket address. - config.add_name_server(NameServerConfig::new(*addr, Protocol::Tcp)); + // `NameServerConfig::udp_and_tcp` bundles both a UDP and a TCP + // connection for one server — TCP is the fallback for responses + // too large for UDP (some TXT / DNSSEC answers). The standard + // DNS port (53) is used. + config.add_name_server(NameServerConfig::udp_and_tcp(addr.ip())); } let mut opts = ResolverOpts::default(); @@ -69,9 +69,10 @@ impl UpstreamResolver { opts.edns0 = true; opts.try_tcp_on_error = true; - let resolver = Resolver::builder_with_config(config, TokioConnectionProvider::default()) + let resolver = Resolver::builder_with_config(config, TokioRuntimeProvider::default()) .with_options(opts) - .build(); + .build() + .ok()?; Some(Self { resolver }) } @@ -101,7 +102,7 @@ impl UpstreamResolver { let (records, response_code) = match lookup_result { Ok(lookup) => { - let recs: Vec = lookup.record_iter().cloned().collect(); + let recs: Vec = lookup.answers().to_vec(); trace!(qname = %qname, qtype = ?qtype, answers = recs.len(), "upstream answer"); (recs, ResponseCode::NoError) } @@ -134,15 +135,15 @@ impl UpstreamResolver { // haven't enumerated yet (HTTPS/SVCB/CAA/…). let answers: Vec<&Record> = records.iter().collect(); - let mut header = Header::response_from_request(info.header); - header.set_response_code(response_code); + let mut metadata = Metadata::response_from_request(info.metadata); + metadata.response_code = response_code; // We are *not* authoritative for the forwarded zone. - header.set_authoritative(false); - header.set_recursion_available(true); + metadata.authoritative = false; + metadata.recursion_available = true; let builder = MessageResponseBuilder::from_message_request(request); let resp = builder.build( - header, + metadata, answers.iter().copied(), std::iter::empty::<&Record>(), std::iter::empty::<&Record>(), @@ -160,9 +161,14 @@ impl UpstreamResolver { } fn error_info(request: &Request, code: ResponseCode) -> ResponseInfo { - let mut header = Header::response_from_request(request.header()); - header.set_response_code(code); - header.into() + // `Request` derefs to `Metadata`; `ResponseInfo` is built from a `Header`. + let mut metadata = Metadata::response_from_request(&request.metadata); + metadata.response_code = code; + Header { + metadata, + counts: HeaderCounts::default(), + } + .into() } #[cfg(test)] diff --git a/crates/temps-dns-resolver/tests/end_to_end.rs b/crates/temps-dns-resolver/tests/end_to_end.rs index 0a9b523a..ef37e2b0 100644 --- a/crates/temps-dns-resolver/tests/end_to_end.rs +++ b/crates/temps-dns-resolver/tests/end_to_end.rs @@ -17,9 +17,9 @@ use std::path::PathBuf; use std::time::Duration; use hickory_resolver::config::{ - NameServerConfig, ResolverConfig as ClientResolverConfig, ResolverOpts, + NameServerConfig, ResolveHosts, ResolverConfig as ClientResolverConfig, ResolverOpts, }; -use hickory_resolver::proto::xfer::Protocol; +use hickory_resolver::net::runtime::TokioRuntimeProvider; use hickory_resolver::TokioResolver; use tempfile::tempdir; use temps_dns_resolver::{ResolverConfig, ResolverHandle}; @@ -60,20 +60,24 @@ fn random_port() -> u16 { } fn client_for(server: SocketAddr) -> TokioResolver { - let mut cfg = ClientResolverConfig::new(); - cfg.add_name_server(NameServerConfig::new(server, Protocol::Udp)); + let mut cfg = ClientResolverConfig::default(); + // The test resolver listens on a random localhost UDP port; point a + // UDP-only nameserver at exactly that address. + let mut name_server = NameServerConfig::udp(server.ip()); + if let Some(conn) = name_server.connections.first_mut() { + conn.port = server.port(); + } + cfg.add_name_server(name_server); let mut opts = ResolverOpts::default(); // Don't fall through to the system resolver — we want hard failures // when our resolver can't answer. - opts.use_hosts_file = hickory_resolver::config::ResolveHosts::Never; + opts.use_hosts_file = ResolveHosts::Never; opts.attempts = 1; opts.timeout = Duration::from_secs(2); - TokioResolver::builder_with_config( - cfg, - hickory_resolver::name_server::TokioConnectionProvider::default(), - ) - .with_options(opts) - .build() + TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default()) + .with_options(opts) + .build() + .expect("failed to build test DNS client") } async fn install_changes_mock(server: &MockServer, body: serde_json::Value) { diff --git a/crates/temps-domains/Cargo.toml b/crates/temps-domains/Cargo.toml index 2a1cf048..f6272d6e 100644 --- a/crates/temps-domains/Cargo.toml +++ b/crates/temps-domains/Cargo.toml @@ -24,7 +24,7 @@ serde_json = { workspace = true } log = { workspace = true } chrono = { workspace = true } sea-orm = { workspace = true } -hickory-resolver = "0.25.2" +hickory-resolver = "0.26" instant-acme = "0.7.2" rcgen = { version = "0.13.2", features = ["x509-parser"] } thiserror = { workspace = true } diff --git a/crates/temps-domains/src/dns_provider.rs b/crates/temps-domains/src/dns_provider.rs index 498ea060..4f9d1033 100644 --- a/crates/temps-domains/src/dns_provider.rs +++ b/crates/temps-domains/src/dns_provider.rs @@ -6,11 +6,11 @@ use cloudflare::framework::{ auth::Credentials, client::async_api::Client, client::ClientConfig, Environment, }; use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts}; -use hickory_resolver::name_server::TokioConnectionProvider; -use hickory_resolver::proto::xfer::Protocol; +use hickory_resolver::net::runtime::TokioRuntimeProvider; +use hickory_resolver::proto::rr::RData; use hickory_resolver::Resolver; use serde::{Deserialize, Serialize}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr}; use std::time::Duration; use tracing::{debug, info, warn}; @@ -714,11 +714,10 @@ impl DnsPropagationChecker { record_name: &str, expected_values: &[String], ) -> DnsServerResult { - // Create resolver config for this specific DNS server using hickory-resolver 0.25+ API - let name_server = - NameServerConfig::new(SocketAddr::new(IpAddr::V4(server.ip), 53), Protocol::Udp); + // Create resolver config for this specific DNS server (hickory 0.26 API). + let name_server = NameServerConfig::udp(IpAddr::V4(server.ip)); - let mut resolver_config = ResolverConfig::new(); + let mut resolver_config = ResolverConfig::default(); resolver_config.add_name_server(name_server); // Configure resolver options @@ -727,20 +726,38 @@ impl DnsPropagationChecker { resolver_opts.attempts = 2; resolver_opts.cache_size = 0; // Disable caching to get fresh results - // Build resolver using the new builder API + // Build resolver using the 0.26 builder API. let resolver = - Resolver::builder_with_config(resolver_config, TokioConnectionProvider::default()) + match Resolver::builder_with_config(resolver_config, TokioRuntimeProvider::default()) .with_options(resolver_opts) - .build(); + .build() + { + Ok(r) => r, + Err(e) => { + return DnsServerResult { + server_name: server.name.to_string(), + server_ip: server.ip.to_string(), + found: false, + values_found: Vec::new(), + error: Some(format!("failed to build DNS resolver: {e}")), + } + } + }; // Query TXT records match resolver.txt_lookup(record_name).await { Ok(lookup) => { let values_found: Vec = lookup + .answers() .iter() - .flat_map(|txt| { - txt.iter() - .map(|data| String::from_utf8_lossy(data).to_string()) + .filter_map(|record| match &record.data { + RData::TXT(txt) => Some( + txt.txt_data + .iter() + .map(|data| String::from_utf8_lossy(data).to_string()) + .collect::(), + ), + _ => None, }) .collect(); diff --git a/crates/temps-domains/src/tls/service.rs b/crates/temps-domains/src/tls/service.rs index 7726898e..2790076e 100644 --- a/crates/temps-domains/src/tls/service.rs +++ b/crates/temps-domains/src/tls/service.rs @@ -1,7 +1,9 @@ use anyhow::Result; use chrono::Utc; -use hickory_resolver::config::{LookupIpStrategy, ResolveHosts, ResolverConfig, ResolverOpts}; -use hickory_resolver::name_server::TokioConnectionProvider; +use hickory_resolver::config::{ + LookupIpStrategy, ResolveHosts, ResolverConfig, ResolverOpts, CLOUDFLARE, +}; +use hickory_resolver::net::runtime::TokioRuntimeProvider; use hickory_resolver::Resolver; use rustls::pki_types::{CertificateDer, PrivateKeyDer}; use std::sync::Arc; @@ -16,7 +18,7 @@ use super::providers::CertificateProvider; use super::repository::CertificateRepository; /// Type alias for the Tokio-based DNS resolver -type TokioResolver = Resolver; +type TokioResolver = Resolver; pub struct TlsService { repository: Arc, @@ -40,13 +42,15 @@ impl TlsService { options.ip_strategy = LookupIpStrategy::Ipv4Only; options.try_tcp_on_error = true; + // Building from a static, known-valid config cannot fail in practice. let resolver = Arc::new( Resolver::builder_with_config( - ResolverConfig::cloudflare(), - TokioConnectionProvider::default(), + ResolverConfig::udp_and_tcp(&CLOUDFLARE), + TokioRuntimeProvider::default(), ) .with_options(options) - .build(), + .build() + .expect("failed to build DNS resolver from static Cloudflare config"), ); Self { @@ -724,14 +728,24 @@ impl TlsService { /// Resolve domain DNS information async fn resolve_domain_info(&self, domain: &str) -> DnsInfo { + use hickory_resolver::proto::rr::{RData, RecordType}; + let mut a_records = Vec::new(); let mut aaaa_records = Vec::new(); let mut error = None; - // Try IPv4 lookup - match self.resolver.ipv4_lookup(domain).await { + // Try IPv4 lookup. The generic `lookup` returns a `Lookup`; pull the + // A rdata out of each answer record (hickory 0.26). + match self.resolver.lookup(domain, RecordType::A).await { Ok(lookup) => { - a_records = lookup.iter().map(|ip| ip.to_string()).collect(); + a_records = lookup + .answers() + .iter() + .filter_map(|record| match &record.data { + RData::A(a) => Some(a.0.to_string()), + _ => None, + }) + .collect(); } Err(e) => { error = Some(format!("IPv4 lookup failed: {}", e)); @@ -739,9 +753,16 @@ impl TlsService { } // Try IPv6 lookup (if IPv4 succeeded or failed, we still try IPv6) - match self.resolver.ipv6_lookup(domain).await { + match self.resolver.lookup(domain, RecordType::AAAA).await { Ok(lookup) => { - aaaa_records = lookup.iter().map(|ip| ip.to_string()).collect(); + aaaa_records = lookup + .answers() + .iter() + .filter_map(|record| match &record.data { + RData::AAAA(aaaa) => Some(aaaa.0.to_string()), + _ => None, + }) + .collect(); } Err(e) => { if error.is_some() { diff --git a/crates/temps-email/Cargo.toml b/crates/temps-email/Cargo.toml index 389ea0ed..152e1d3f 100644 --- a/crates/temps-email/Cargo.toml +++ b/crates/temps-email/Cargo.toml @@ -52,10 +52,11 @@ uuid = { workspace = true } urlencoding = { workspace = true } # DNS resolution -hickory-resolver = "0.24" +hickory-resolver = "0.26" -# Email validation -check-if-email-exists = "0.11" +# Email validation (native): SMTP probing over SOCKS5 + Gravatar hashing +tokio-socks = "0.5" +md5 = "0.7" [dev-dependencies] tokio-test = "0.4" diff --git a/crates/temps-email/src/dns.rs b/crates/temps-email/src/dns.rs index c5de2ed3..48df271f 100644 --- a/crates/temps-email/src/dns.rs +++ b/crates/temps-email/src/dns.rs @@ -1,14 +1,16 @@ //! DNS verification utilities for email records -use hickory_resolver::config::{ResolverConfig, ResolverOpts}; -use hickory_resolver::TokioAsyncResolver; +use hickory_resolver::config::{ResolveHosts, ResolverConfig, ResolverOpts, CLOUDFLARE}; +use hickory_resolver::net::runtime::TokioRuntimeProvider; +use hickory_resolver::proto::rr::{RData, RecordType}; +use hickory_resolver::Resolver; use tracing::debug; use crate::providers::DnsRecordStatus; /// DNS verification service for checking email-related DNS records pub struct DnsVerifier { - resolver: TokioAsyncResolver, + resolver: Resolver, } impl Default for DnsVerifier { @@ -22,9 +24,17 @@ impl DnsVerifier { pub fn new() -> Self { let mut options = ResolverOpts::default(); options.try_tcp_on_error = true; - options.use_hosts_file = false; - - let resolver = TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), options); + options.use_hosts_file = ResolveHosts::Never; + + // Building from a static, known-valid config cannot fail in practice; + // a failure here means the process environment is fundamentally broken. + let resolver = Resolver::builder_with_config( + ResolverConfig::udp_and_tcp(&CLOUDFLARE), + TokioRuntimeProvider::default(), + ) + .with_options(options) + .build() + .expect("failed to build DNS resolver from static Cloudflare config"); Self { resolver } } @@ -35,9 +45,12 @@ impl DnsVerifier { match self.resolver.txt_lookup(name).await { Ok(lookup) => { - for record in lookup.iter() { - let txt_data: String = record - .txt_data() + for record in lookup.answers() { + let RData::TXT(txt) = &record.data else { + continue; + }; + let txt_data: String = txt + .txt_data .iter() .map(|data| String::from_utf8_lossy(data).to_string()) .collect(); @@ -76,14 +89,10 @@ impl DnsVerifier { pub async fn verify_cname_record(&self, name: &str, expected_value: &str) -> DnsRecordStatus { debug!("Verifying CNAME record: {} -> {}", name, expected_value); - match self - .resolver - .lookup(name, hickory_resolver::proto::rr::RecordType::CNAME) - .await - { + match self.resolver.lookup(name, RecordType::CNAME).await { Ok(lookup) => { - for record in lookup.iter() { - if let Some(cname) = record.as_cname() { + for record in lookup.answers() { + if let RData::CNAME(cname) = &record.data { let cname_str = cname.to_string(); debug!("Found CNAME record: {}", cname_str); @@ -124,9 +133,12 @@ impl DnsVerifier { match self.resolver.mx_lookup(name).await { Ok(lookup) => { - for record in lookup.iter() { - let exchange = record.exchange().to_string(); - let priority = record.preference(); + for record in lookup.answers() { + let RData::MX(mx) = &record.data else { + continue; + }; + let exchange = mx.exchange.to_string(); + let priority = mx.preference; debug!("Found MX record: {} (priority: {})", exchange, priority); @@ -168,9 +180,12 @@ impl DnsVerifier { match self.resolver.txt_lookup(domain).await { Ok(lookup) => { - for record in lookup.iter() { - let txt_data: String = record - .txt_data() + for record in lookup.answers() { + let RData::TXT(txt) = &record.data else { + continue; + }; + let txt_data: String = txt + .txt_data .iter() .map(|data| String::from_utf8_lossy(data).to_string()) .collect(); diff --git a/crates/temps-email/src/services/mod.rs b/crates/temps-email/src/services/mod.rs index 107f3516..899e5ec3 100644 --- a/crates/temps-email/src/services/mod.rs +++ b/crates/temps-email/src/services/mod.rs @@ -6,7 +6,7 @@ mod provider_service; mod tracking_service; #[cfg(test)] mod tracking_service_integration_tests; -mod validation_service; +mod validation; pub use domain_service::{CreateDomainRequest, DomainService, DomainWithDnsRecords}; pub use email_service::{ @@ -17,7 +17,7 @@ pub use provider_service::{ CreateProviderRequest, ProviderCredentials, ProviderService, TestEmailResult, }; pub use tracking_service::{ExtractedLink, TrackingEvent, TrackingService, TransformResult}; -pub use validation_service::{ +pub use validation::{ MiscResult, MxResult, ProxyConfig, ReachabilityStatus, SmtpResult, SyntaxResult, ValidateEmailRequest, ValidateEmailResponse, ValidationConfig, ValidationService, }; diff --git a/crates/temps-email/src/services/validation/misc.rs b/crates/temps-email/src/services/validation/misc.rs new file mode 100644 index 00000000..ee9a5480 --- /dev/null +++ b/crates/temps-email/src/services/validation/misc.rs @@ -0,0 +1,151 @@ +//! Miscellaneous email signals: disposable-provider, role-account, and +//! B2C-provider detection, plus the Gravatar profile-image URL. +//! +//! The lists here are deliberately small and high-signal — they cover the +//! providers that actually move the needle for deliverability decisions +//! rather than attempting an exhaustive catalogue. + +/// A non-exhaustive list of well-known disposable / throwaway email domains. +const DISPOSABLE_DOMAINS: &[&str] = &[ + "10minutemail.com", + "guerrillamail.com", + "guerrillamail.net", + "mailinator.com", + "tempmail.com", + "temp-mail.org", + "throwawaymail.com", + "yopmail.com", + "trashmail.com", + "getnada.com", + "maildrop.cc", + "dispostable.com", + "fakeinbox.com", + "sharklasers.com", + "spam4.me", + "mailnesia.com", + "mintemail.com", + "mohmal.com", + "emailondeck.com", + "tempinbox.com", +]; + +/// Local-parts that denote a shared / role mailbox rather than a person. +const ROLE_LOCAL_PARTS: &[&str] = &[ + "admin", + "administrator", + "billing", + "contact", + "help", + "hello", + "hostmaster", + "info", + "mail", + "marketing", + "noc", + "no-reply", + "noreply", + "office", + "postmaster", + "root", + "sales", + "security", + "support", + "sysadmin", + "team", + "webmaster", + "abuse", + "privacy", + "legal", +]; + +/// Consumer (B2C) mailbox providers — a free personal address rather than a +/// company domain. +const B2C_DOMAINS: &[&str] = &[ + "gmail.com", + "googlemail.com", + "yahoo.com", + "yahoo.co.uk", + "ymail.com", + "hotmail.com", + "hotmail.co.uk", + "outlook.com", + "live.com", + "msn.com", + "icloud.com", + "me.com", + "mac.com", + "aol.com", + "protonmail.com", + "proton.me", + "gmx.com", + "gmx.net", + "mail.com", + "zoho.com", + "yandex.com", +]; + +/// Whether the domain is a known disposable / throwaway provider. +pub fn is_disposable(domain: &str) -> bool { + let d = domain.to_ascii_lowercase(); + DISPOSABLE_DOMAINS.contains(&d.as_str()) +} + +/// Whether the local-part denotes a role / shared mailbox (e.g. `info@`). +pub fn is_role_account(local_part: &str) -> bool { + let l = local_part.to_ascii_lowercase(); + ROLE_LOCAL_PARTS.contains(&l.as_str()) +} + +/// Whether the domain is a known consumer (B2C) mailbox provider. +pub fn is_b2c(domain: &str) -> bool { + let d = domain.to_ascii_lowercase(); + B2C_DOMAINS.contains(&d.as_str()) +} + +/// Build the Gravatar profile-image URL for an address. Gravatar keys on the +/// MD5 of the lowercased, trimmed address; `d=404` makes the URL 404 when no +/// avatar exists so callers can probe for presence. +pub fn gravatar_url(email: &str) -> String { + let normalized = email.trim().to_ascii_lowercase(); + let digest = md5::compute(normalized.as_bytes()); + format!("https://www.gravatar.com/avatar/{digest:x}?d=404") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_disposable() { + assert!(is_disposable("mailinator.com")); + assert!(is_disposable("Guerrillamail.com")); // case-insensitive + assert!(!is_disposable("gmail.com")); + assert!(!is_disposable("acme-corp.com")); + } + + #[test] + fn test_role_account() { + assert!(is_role_account("info")); + assert!(is_role_account("ADMIN")); + assert!(is_role_account("no-reply")); + assert!(!is_role_account("john.smith")); + assert!(!is_role_account("alice")); + } + + #[test] + fn test_b2c() { + assert!(is_b2c("gmail.com")); + assert!(is_b2c("Outlook.com")); + assert!(!is_b2c("acme-corp.com")); + } + + #[test] + fn test_gravatar_url() { + // Known MD5 of "test@example.com". + let url = gravatar_url("test@example.com"); + assert!(url.starts_with("https://www.gravatar.com/avatar/")); + assert!(url.ends_with("?d=404")); + // Normalization: case and surrounding whitespace must not matter. + assert_eq!(gravatar_url(" Test@Example.COM "), url); + } +} diff --git a/crates/temps-email/src/services/validation/mod.rs b/crates/temps-email/src/services/validation/mod.rs new file mode 100644 index 00000000..5729dc2c --- /dev/null +++ b/crates/temps-email/src/services/validation/mod.rs @@ -0,0 +1,434 @@ +//! Native email-address validation engine. +//! +//! Replaces the former `check-if-email-exists` dependency (AGPL-licensed, and +//! pinned to an old `hickory` with open CVEs). Four stages — syntax, MX, +//! misc signals, and SMTP probing — combine into an overall reachability +//! verdict, without ever delivering a message. + +mod misc; +mod mx; +mod smtp; +mod syntax; + +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use tracing::{debug, info}; + +use crate::errors::EmailError; + +/// SOCKS5 proxy configuration for routing SMTP probes. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProxyConfig { + pub host: String, + pub port: u16, + pub username: Option, + pub password: Option, +} + +/// Configuration for the validation service. +#[derive(Debug, Clone, Default)] +pub struct ValidationConfig { + /// SOCKS5 proxy applied to every probe (per-request proxy overrides it). + pub proxy: Option, + /// Envelope sender used in `MAIL FROM` during SMTP probing. + pub from_email: Option, + /// Name announced in the SMTP `EHLO` command. + pub hello_name: Option, +} + +/// Request to validate a single email address. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidateEmailRequest { + pub email: String, + /// Optional per-request SOCKS5 proxy (overrides the service default). + pub proxy: Option, +} + +/// Overall deliverability verdict. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ReachabilityStatus { + /// Safe to send to. + Safe, + /// May bounce — proceed with caution. + Risky, + /// Invalid; will definitely bounce. + Invalid, + /// Could not be determined. + Unknown, +} + +/// Syntax-stage result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SyntaxResult { + pub is_valid_syntax: bool, + pub domain: Option, + pub username: Option, + pub suggestion: Option, +} + +/// MX-stage result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MxResult { + pub accepts_mail: bool, + pub records: Vec, + pub error: Option, +} + +/// Misc-signals result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MiscResult { + pub is_disposable: bool, + pub is_role_account: bool, + pub is_b2c: bool, + pub gravatar_url: Option, +} + +/// SMTP-stage result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SmtpResult { + pub can_connect_smtp: bool, + pub has_full_inbox: bool, + pub is_catch_all: bool, + pub is_deliverable: bool, + pub is_disabled: bool, + pub error: Option, +} + +/// Complete validation response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ValidateEmailResponse { + pub email: String, + pub is_reachable: ReachabilityStatus, + pub syntax: SyntaxResult, + pub mx: MxResult, + pub misc: MiscResult, + pub smtp: SmtpResult, +} + +/// Service for validating email addresses. +pub struct ValidationService { + config: ValidationConfig, +} + +impl ValidationService { + /// Create a validation service with the given configuration. + pub fn new(config: ValidationConfig) -> Self { + Self { config } + } + + /// Create a validation service with default configuration. + pub fn with_default_config() -> Self { + Self { + config: ValidationConfig::default(), + } + } + + /// Validate a single email address. Never sends a message — the SMTP + /// probe stops before `DATA`. + pub async fn validate( + &self, + request: ValidateEmailRequest, + ) -> Result { + info!("Validating email: {}", request.email); + + // ── Stage 1: syntax ───────────────────────────────────────────── + let parsed = syntax::parse_email(&request.email); + let syntax = match &parsed { + Some(p) => SyntaxResult { + is_valid_syntax: true, + domain: Some(p.domain.clone()), + username: Some(p.local_part.clone()), + suggestion: syntax::suggest_correction(p), + }, + None => SyntaxResult { + is_valid_syntax: false, + domain: None, + username: None, + suggestion: None, + }, + }; + + // Invalid syntax is terminal — nothing else is worth checking. + let Some(parsed) = parsed else { + return Ok(ValidateEmailResponse { + email: request.email.clone(), + is_reachable: ReachabilityStatus::Invalid, + syntax, + mx: MxResult { + accepts_mail: false, + records: Vec::new(), + error: None, + }, + misc: MiscResult { + is_disposable: false, + is_role_account: false, + is_b2c: false, + gravatar_url: None, + }, + smtp: SmtpResult { + can_connect_smtp: false, + has_full_inbox: false, + is_catch_all: false, + is_deliverable: false, + is_disabled: false, + error: None, + }, + }); + }; + + // ── Stage 2: misc signals (no network) ────────────────────────── + let misc = MiscResult { + is_disposable: misc::is_disposable(&parsed.domain), + is_role_account: misc::is_role_account(&parsed.local_part), + is_b2c: misc::is_b2c(&parsed.domain), + gravatar_url: Some(misc::gravatar_url(&request.email)), + }; + + // ── Stage 3: MX lookup ────────────────────────────────────────── + let mx_records = mx::lookup_mx(&parsed.domain).await; + let mx = MxResult { + accepts_mail: mx_records.accepts_mail(), + records: mx_records.hosts.clone(), + error: mx_records.error.clone(), + }; + + // No MX → the domain cannot receive mail; terminal Invalid. + if !mx.accepts_mail { + return Ok(ValidateEmailResponse { + email: request.email.clone(), + is_reachable: ReachabilityStatus::Invalid, + syntax, + mx, + misc, + smtp: SmtpResult { + can_connect_smtp: false, + has_full_inbox: false, + is_catch_all: false, + is_deliverable: false, + is_disabled: false, + error: None, + }, + }); + } + + // ── Stage 4: SMTP probe ───────────────────────────────────────── + let proxy = request.proxy.as_ref().or(self.config.proxy.as_ref()); + let from_email = self + .config + .from_email + .as_deref() + .unwrap_or("noreply@temps.sh"); + let hello_name = self.config.hello_name.as_deref().unwrap_or("temps.sh"); + + let probe = smtp::probe_mailbox(smtp::SmtpProbeConfig { + mx_hosts: &mx_records.hosts, + to_email: &request.email, + from_email, + hello_name, + timeout: Duration::from_secs(10), + proxy, + }) + .await; + + let smtp = SmtpResult { + can_connect_smtp: probe.can_connect, + has_full_inbox: probe.has_full_inbox, + is_catch_all: probe.is_catch_all, + is_deliverable: probe.is_deliverable, + is_disabled: probe.is_disabled, + error: probe.error.clone(), + }; + + let is_reachable = reachability(&misc, &smtp); + debug!( + "Email validation result for {}: is_reachable={:?}", + request.email, is_reachable + ); + + Ok(ValidateEmailResponse { + email: request.email, + is_reachable, + syntax, + mx, + misc, + smtp, + }) + } + + /// Validate several addresses sequentially. + pub async fn validate_batch( + &self, + emails: Vec, + ) -> Result, EmailError> { + let mut results = Vec::with_capacity(emails.len()); + for email in emails { + results.push( + self.validate(ValidateEmailRequest { email, proxy: None }) + .await?, + ); + } + Ok(results) + } +} + +/// Combine misc + SMTP signals into the overall verdict. Syntax/MX failures +/// are handled before this point and never reach here. +fn reachability(misc: &MiscResult, smtp: &SmtpResult) -> ReachabilityStatus { + // Could not reach any mail server, or the server wouldn't tell us — we + // genuinely do not know. + if !smtp.can_connect_smtp { + return ReachabilityStatus::Unknown; + } + if smtp.error.is_some() && !smtp.is_deliverable { + return ReachabilityStatus::Unknown; + } + + // Mailbox explicitly does not exist (server reached, not deliverable, not + // catch-all, no soft error) → Invalid. + if !smtp.is_deliverable && !smtp.is_catch_all { + return ReachabilityStatus::Invalid; + } + + // From here the address is accepted. Decide Safe vs Risky. + if smtp.is_catch_all + || smtp.is_disabled + || smtp.has_full_inbox + || misc.is_disposable + || misc.is_role_account + { + return ReachabilityStatus::Risky; + } + + ReachabilityStatus::Safe +} + +#[cfg(test)] +mod tests { + use super::*; + + fn smtp(can_connect: bool, deliverable: bool) -> SmtpResult { + SmtpResult { + can_connect_smtp: can_connect, + has_full_inbox: false, + is_catch_all: false, + is_deliverable: deliverable, + is_disabled: false, + error: None, + } + } + + fn misc(disposable: bool, role: bool) -> MiscResult { + MiscResult { + is_disposable: disposable, + is_role_account: role, + is_b2c: false, + gravatar_url: None, + } + } + + #[test] + fn test_reachability_safe() { + assert_eq!( + reachability(&misc(false, false), &smtp(true, true)), + ReachabilityStatus::Safe + ); + } + + #[test] + fn test_reachability_invalid_mailbox() { + assert_eq!( + reachability(&misc(false, false), &smtp(true, false)), + ReachabilityStatus::Invalid + ); + } + + #[test] + fn test_reachability_unknown_when_unreachable() { + assert_eq!( + reachability(&misc(false, false), &smtp(false, false)), + ReachabilityStatus::Unknown + ); + } + + #[test] + fn test_reachability_risky_disposable() { + // Deliverable but from a disposable provider → Risky, not Safe. + assert_eq!( + reachability(&misc(true, false), &smtp(true, true)), + ReachabilityStatus::Risky + ); + } + + #[test] + fn test_reachability_risky_role_account() { + assert_eq!( + reachability(&misc(false, true), &smtp(true, true)), + ReachabilityStatus::Risky + ); + } + + #[test] + fn test_reachability_risky_catch_all() { + let mut s = smtp(true, false); + s.is_catch_all = true; + assert_eq!( + reachability(&misc(false, false), &s), + ReachabilityStatus::Risky + ); + } + + #[test] + fn test_reachability_unknown_on_soft_error() { + let mut s = smtp(true, false); + s.error = Some("MAIL FROM rejected: 421 try later".to_string()); + assert_eq!( + reachability(&misc(false, false), &s), + ReachabilityStatus::Unknown + ); + } + + #[tokio::test] + async fn test_validate_invalid_syntax() { + let service = ValidationService::with_default_config(); + let resp = service + .validate(ValidateEmailRequest { + email: "not-an-email".to_string(), + proxy: None, + }) + .await + .unwrap(); + assert!(!resp.syntax.is_valid_syntax); + assert_eq!(resp.is_reachable, ReachabilityStatus::Invalid); + // Invalid syntax short-circuits before any network call. + assert!(!resp.mx.accepts_mail); + assert!(!resp.smtp.can_connect_smtp); + } + + #[tokio::test] + async fn test_validate_syntax_ok_extracts_parts() { + let service = ValidationService::with_default_config(); + // Use an MX-less reserved domain so the test stays offline-safe: + // .invalid never resolves, so validation stops at the MX stage. + let resp = service + .validate(ValidateEmailRequest { + email: "alice@nonexistent-temps-test.invalid".to_string(), + proxy: None, + }) + .await + .unwrap(); + assert!(resp.syntax.is_valid_syntax); + assert_eq!(resp.syntax.username.as_deref(), Some("alice")); + assert_eq!( + resp.syntax.domain.as_deref(), + Some("nonexistent-temps-test.invalid") + ); + } + + #[test] + fn test_config_default() { + let c = ValidationConfig::default(); + assert!(c.proxy.is_none() && c.from_email.is_none() && c.hello_name.is_none()); + } +} diff --git a/crates/temps-email/src/services/validation/mx.rs b/crates/temps-email/src/services/validation/mx.rs new file mode 100644 index 00000000..12d7eb2d --- /dev/null +++ b/crates/temps-email/src/services/validation/mx.rs @@ -0,0 +1,134 @@ +//! MX-record resolution for a domain. +//! +//! Uses `hickory-resolver` configured against Cloudflare DNS. The set of MX +//! hosts (ordered by preference, lowest = highest priority) feeds the SMTP +//! probing stage. + +use hickory_resolver::config::{ResolveHosts, ResolverConfig, ResolverOpts, CLOUDFLARE}; +use hickory_resolver::net::runtime::TokioRuntimeProvider; +use hickory_resolver::proto::rr::RData; +use hickory_resolver::Resolver; +use tracing::debug; + +/// MX records for a domain, ordered by ascending preference (most-preferred +/// mail server first). +#[derive(Debug, Clone, Default)] +pub struct MxRecords { + /// MX exchange host names, most-preferred first. Trailing dots stripped. + pub hosts: Vec, + /// Lookup error, when resolution failed for a reason other than "no MX". + pub error: Option, +} + +impl MxRecords { + /// Whether the domain advertises at least one mail exchanger. + pub fn accepts_mail(&self) -> bool { + !self.hosts.is_empty() + } +} + +/// Resolve the MX records for `domain` using Cloudflare DNS. +pub async fn lookup_mx(domain: &str) -> MxRecords { + let mut opts = ResolverOpts::default(); + opts.try_tcp_on_error = true; + opts.use_hosts_file = ResolveHosts::Never; + + let resolver = match Resolver::builder_with_config( + ResolverConfig::udp_and_tcp(&CLOUDFLARE), + TokioRuntimeProvider::default(), + ) + .with_options(opts) + .build() + { + Ok(r) => r, + Err(e) => { + return MxRecords { + hosts: Vec::new(), + error: Some(format!("failed to build DNS resolver: {e}")), + } + } + }; + + match resolver.mx_lookup(domain).await { + Ok(lookup) => { + // `mx_lookup` yields a generic `Lookup`; pull the MX rdata out of + // each answer record. Collect (preference, exchange) then sort so + // the most-preferred server is probed first. + let mut records: Vec<(u16, String)> = lookup + .answers() + .iter() + .filter_map(|record| match &record.data { + RData::MX(mx) => Some(( + mx.preference, + mx.exchange.to_string().trim_end_matches('.').to_string(), + )), + _ => None, + }) + .filter(|(_, host)| !host.is_empty()) + .collect(); + records.sort_by_key(|(pref, _)| *pref); + + let hosts: Vec = records.into_iter().map(|(_, host)| host).collect(); + debug!("MX lookup for {domain}: {} record(s)", hosts.len()); + MxRecords { hosts, error: None } + } + Err(e) => { + // No-records / NXDOMAIN is a normal "domain does not accept mail" + // answer, not a lookup failure — surface it as empty, no error. + let msg = e.to_string(); + if msg.contains("no record") || msg.contains("NXDomain") || e.is_no_records_found() { + debug!("MX lookup for {domain}: no records"); + MxRecords { + hosts: Vec::new(), + error: None, + } + } else { + debug!("MX lookup for {domain} failed: {msg}"); + MxRecords { + hosts: Vec::new(), + error: Some(msg), + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_accepts_mail() { + let empty = MxRecords::default(); + assert!(!empty.accepts_mail()); + + let with_hosts = MxRecords { + hosts: vec!["mx.example.com".to_string()], + error: None, + }; + assert!(with_hosts.accepts_mail()); + } + + #[tokio::test] + async fn test_lookup_real_mx() { + if std::env::var("TEMPS_NETWORK_TESTS").is_err() { + println!("Network tests disabled; set TEMPS_NETWORK_TESTS=1 to enable"); + return; + } + // gmail.com always has MX records. + let mx = lookup_mx("gmail.com").await; + assert!(mx.accepts_mail()); + assert!(mx.error.is_none()); + } + + #[tokio::test] + async fn test_lookup_domain_without_mx() { + if std::env::var("TEMPS_NETWORK_TESTS").is_err() { + println!("Network tests disabled; set TEMPS_NETWORK_TESTS=1 to enable"); + return; + } + // A non-existent domain must come back as "no mail", not an error. + let mx = lookup_mx("this-domain-definitely-does-not-exist-temps.invalid").await; + assert!(!mx.accepts_mail()); + } +} diff --git a/crates/temps-email/src/services/validation/smtp.rs b/crates/temps-email/src/services/validation/smtp.rs new file mode 100644 index 00000000..933bdfc4 --- /dev/null +++ b/crates/temps-email/src/services/validation/smtp.rs @@ -0,0 +1,375 @@ +//! SMTP-level mailbox probing. +//! +//! We open a plain TCP connection to a domain's mail exchanger and run the +//! SMTP envelope handshake up to `RCPT TO` *without ever sending `DATA`* — +//! i.e. we never deliver a message. The server's reply to `RCPT TO` tells us +//! whether the mailbox is deliverable. +//! +//! Catch-all detection: we additionally probe a random, almost-certainly +//! non-existent local-part. If that is also accepted, the domain accepts all +//! addresses and a "deliverable" result for the real address is unreliable. +//! +//! Optional SOCKS5 proxying (`ProxyConfig`) routes the TCP connection through +//! a proxy — necessary because many networks block outbound port 25. + +use std::time::Duration; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::timeout; +use tracing::debug; + +use super::ProxyConfig; + +/// Result of probing a single mailbox over SMTP. +#[derive(Debug, Clone, Default)] +pub struct SmtpProbe { + pub can_connect: bool, + pub is_deliverable: bool, + pub is_disabled: bool, + pub has_full_inbox: bool, + pub is_catch_all: bool, + pub error: Option, +} + +/// Settings for an SMTP probe. +pub struct SmtpProbeConfig<'a> { + /// MX hosts to try, in preference order. + pub mx_hosts: &'a [String], + /// Address being verified. + pub to_email: &'a str, + /// Envelope sender used in `MAIL FROM`. + pub from_email: &'a str, + /// Name announced in `EHLO`. + pub hello_name: &'a str, + /// Per-operation timeout. + pub timeout: Duration, + /// Optional SOCKS5 proxy. + pub proxy: Option<&'a ProxyConfig>, +} + +/// A duplex stream we can run SMTP over — either a direct TCP connection or +/// one tunnelled through a SOCKS5 proxy. Both implement `AsyncRead`/`Write`. +enum SmtpStream { + Direct(TcpStream), + Proxied(tokio_socks::tcp::Socks5Stream), +} + +impl SmtpStream { + async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + SmtpStream::Direct(s) => s.read(buf).await, + SmtpStream::Proxied(s) => s.read(buf).await, + } + } + async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + match self { + SmtpStream::Direct(s) => s.write_all(buf).await, + SmtpStream::Proxied(s) => s.write_all(buf).await, + } + } +} + +/// Probe a mailbox. Tries each MX host until one accepts a TCP connection; +/// the first reachable host decides the result. +pub async fn probe_mailbox(config: SmtpProbeConfig<'_>) -> SmtpProbe { + if config.mx_hosts.is_empty() { + return SmtpProbe { + error: Some("no MX hosts to probe".to_string()), + ..Default::default() + }; + } + + let mut last_error = None; + for host in config.mx_hosts { + match probe_single_host(host, &config).await { + Ok(probe) => return probe, + Err(e) => { + debug!("SMTP probe via {host} failed: {e}"); + last_error = Some(e); + } + } + } + + SmtpProbe { + can_connect: false, + error: last_error.or_else(|| Some("all MX hosts unreachable".to_string())), + ..Default::default() + } +} + +/// Run the full SMTP conversation against one MX host. Returns `Err` only +/// when the host could not be reached at all (so the caller can try the next +/// MX); a reachable host that rejects the mailbox is still `Ok`. +async fn probe_single_host(host: &str, config: &SmtpProbeConfig<'_>) -> Result { + let addr = format!("{host}:25"); + let mut stream = connect(&addr, config).await?; + + // Greeting. + let greeting = read_reply(&mut stream, config.timeout).await?; + if !greeting.starts_with('2') { + return Err(format!("server greeting was not 2xx: {greeting}")); + } + + // EHLO. + send( + &mut stream, + &format!("EHLO {}\r\n", config.hello_name), + config.timeout, + ) + .await?; + let _ = read_reply(&mut stream, config.timeout).await?; + + // MAIL FROM — envelope sender. + send( + &mut stream, + &format!("MAIL FROM:<{}>\r\n", config.from_email), + config.timeout, + ) + .await?; + let mail_reply = read_reply(&mut stream, config.timeout).await?; + if !mail_reply.starts_with('2') { + // Connected, but the server won't take our envelope sender — we can't + // determine deliverability. Reachable, but Unknown. + let _ = send(&mut stream, "QUIT\r\n", config.timeout).await; + return Ok(SmtpProbe { + can_connect: true, + error: Some(format!("MAIL FROM rejected: {mail_reply}")), + ..Default::default() + }); + } + + // RCPT TO — the real address under test. + let real = rcpt_outcome(&mut stream, config.to_email, config.timeout).await?; + + // Catch-all probe: a random local-part that should not exist. + let domain = config.to_email.rsplit('@').next().unwrap_or_default(); + let random_addr = format!("temps-probe-{}@{}", random_token(), domain); + let catch_all = match rcpt_outcome(&mut stream, &random_addr, config.timeout).await { + Ok(o) => o.deliverable, + Err(_) => false, + }; + + let _ = send(&mut stream, "QUIT\r\n", config.timeout).await; + + Ok(SmtpProbe { + can_connect: true, + // On a catch-all domain a 250 for the real address is meaningless. + is_deliverable: real.deliverable && !catch_all, + is_disabled: real.disabled, + has_full_inbox: real.full_inbox, + is_catch_all: catch_all, + error: None, + }) +} + +/// Per-`RCPT TO` interpretation. +struct RcptOutcome { + deliverable: bool, + disabled: bool, + full_inbox: bool, +} + +/// Send a single `RCPT TO` and classify the reply. +async fn rcpt_outcome( + stream: &mut SmtpStream, + address: &str, + op_timeout: Duration, +) -> Result { + send(stream, &format!("RCPT TO:<{address}>\r\n",), op_timeout).await?; + let reply = read_reply(stream, op_timeout).await?; + Ok(classify_rcpt_reply(&reply)) +} + +/// Map an SMTP `RCPT TO` reply to a deliverability outcome. +/// +/// - `2xx` → mailbox accepted (deliverable). +/// - `552` / "quota"/"full" wording → mailbox exists but inbox is full. +/// - `5xx` with "disabled"/"suspended" wording → mailbox disabled. +/// - other `5xx` → mailbox does not exist (not deliverable, not disabled). +/// - `4xx` → temporary failure; treated as not-deliverable / Unknown upstream. +fn classify_rcpt_reply(reply: &str) -> RcptOutcome { + let lower = reply.to_ascii_lowercase(); + if reply.starts_with('2') { + return RcptOutcome { + deliverable: true, + disabled: false, + full_inbox: false, + }; + } + let full_inbox = reply.starts_with("552") + || lower.contains("quota") + || lower.contains("inbox is full") + || lower.contains("mailbox full"); + let disabled = lower.contains("disabled") + || lower.contains("suspended") + || lower.contains("inactive") + || lower.contains("not in use"); + RcptOutcome { + deliverable: false, + disabled, + full_inbox, + } +} + +/// Open the connection — direct or via SOCKS5 — applying the connect timeout. +async fn connect(addr: &str, config: &SmtpProbeConfig<'_>) -> Result { + match config.proxy { + Some(proxy) => { + let proxy_addr = format!("{}:{}", proxy.host, proxy.port); + let connect = async { + match (&proxy.username, &proxy.password) { + (Some(user), Some(pass)) => { + tokio_socks::tcp::Socks5Stream::connect_with_password( + proxy_addr.as_str(), + addr, + user.as_str(), + pass.as_str(), + ) + .await + } + _ => tokio_socks::tcp::Socks5Stream::connect(proxy_addr.as_str(), addr).await, + } + }; + timeout(config.timeout, connect) + .await + .map_err(|_| format!("SOCKS5 connect to {addr} timed out"))? + .map(SmtpStream::Proxied) + .map_err(|e| format!("SOCKS5 connect to {addr} failed: {e}")) + } + None => timeout(config.timeout, TcpStream::connect(addr)) + .await + .map_err(|_| format!("TCP connect to {addr} timed out"))? + .map(SmtpStream::Direct) + .map_err(|e| format!("TCP connect to {addr} failed: {e}")), + } +} + +/// Write a command, bounded by the operation timeout. +async fn send(stream: &mut SmtpStream, cmd: &str, op_timeout: Duration) -> Result<(), String> { + timeout(op_timeout, stream.write_all(cmd.as_bytes())) + .await + .map_err(|_| "SMTP write timed out".to_string())? + .map_err(|e| format!("SMTP write failed: {e}")) +} + +/// Read one SMTP reply. Handles multi-line replies (`250-…` continuation +/// lines) by reading until a line whose 4th byte is a space. Returns the +/// final line, which carries the authoritative status code. +async fn read_reply(stream: &mut SmtpStream, op_timeout: Duration) -> Result { + let mut buf = Vec::with_capacity(512); + let mut chunk = [0u8; 512]; + + loop { + let n = timeout(op_timeout, stream.read(&mut chunk)) + .await + .map_err(|_| "SMTP read timed out".to_string())? + .map_err(|e| format!("SMTP read failed: {e}"))?; + if n == 0 { + return Err("SMTP connection closed by server".to_string()); + } + buf.extend_from_slice(&chunk[..n]); + + // A complete reply ends with a line of the form "NNN \r\n" + // (space after the code, not a hyphen). + if let Some(last_line) = last_complete_line(&buf) { + if is_final_reply_line(&last_line) { + return Ok(last_line); + } + } + if buf.len() > 64 * 1024 { + return Err("SMTP reply exceeded 64 KiB".to_string()); + } + } +} + +/// Return the last CRLF-terminated line in `buf`, if any. +fn last_complete_line(buf: &[u8]) -> Option { + let text = String::from_utf8_lossy(buf); + let trimmed = text.trim_end_matches(['\r', '\n']); + if !text.ends_with('\n') { + return None; + } + trimmed.rsplit("\r\n").next().map(|s| s.to_string()) +} + +/// A final SMTP reply line has a space (not `-`) as its 4th character. +fn is_final_reply_line(line: &str) -> bool { + let b = line.as_bytes(); + b.len() >= 4 && b[0].is_ascii_digit() && b[3] == b' ' +} + +/// A short random token for the catch-all probe local-part. +fn random_token() -> String { + // Derive from a UUIDv4 — no extra RNG dependency, plenty of entropy. + uuid::Uuid::new_v4().simple().to_string()[..16].to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_classify_deliverable() { + let o = classify_rcpt_reply("250 2.1.5 OK"); + assert!(o.deliverable && !o.disabled && !o.full_inbox); + } + + #[test] + fn test_classify_nonexistent() { + let o = classify_rcpt_reply("550 5.1.1 user unknown"); + assert!(!o.deliverable && !o.disabled); + } + + #[test] + fn test_classify_full_inbox() { + assert!(classify_rcpt_reply("552 mailbox full").full_inbox); + assert!(classify_rcpt_reply("450 4.2.2 quota exceeded").full_inbox); + } + + #[test] + fn test_classify_disabled() { + assert!(classify_rcpt_reply("550 5.2.1 mailbox disabled").disabled); + assert!(classify_rcpt_reply("550 account suspended").disabled); + } + + #[test] + fn test_final_reply_line() { + assert!(is_final_reply_line("250 OK")); + assert!(!is_final_reply_line("250-PIPELINING")); + assert!(!is_final_reply_line("foo")); + } + + #[test] + fn test_last_complete_line_multiline() { + let buf = b"250-PIPELINING\r\n250-SIZE 1024\r\n250 HELP\r\n"; + assert_eq!(last_complete_line(buf), Some("250 HELP".to_string())); + } + + #[test] + fn test_last_complete_line_incomplete() { + // No trailing newline → reply not yet complete. + assert_eq!(last_complete_line(b"250 HEL"), None); + } + + #[test] + fn test_random_token_unique() { + assert_ne!(random_token(), random_token()); + assert_eq!(random_token().len(), 16); + } + + #[tokio::test] + async fn test_probe_no_mx_hosts() { + let probe = probe_mailbox(SmtpProbeConfig { + mx_hosts: &[], + to_email: "test@example.com", + from_email: "noreply@temps.sh", + hello_name: "temps.sh", + timeout: Duration::from_secs(1), + proxy: None, + }) + .await; + assert!(!probe.can_connect); + assert!(probe.error.is_some()); + } +} diff --git a/crates/temps-email/src/services/validation/syntax.rs b/crates/temps-email/src/services/validation/syntax.rs new file mode 100644 index 00000000..101ad208 --- /dev/null +++ b/crates/temps-email/src/services/validation/syntax.rs @@ -0,0 +1,197 @@ +//! Email address syntax validation. +//! +//! Pragmatic RFC 5321/5322 parsing — strict enough to reject the addresses +//! that always bounce, lenient enough not to reject deliverable ones. We do +//! not attempt full RFC 5322 (quoted local-parts, comments) because such +//! addresses are vanishingly rare and SMTP probing catches the rest. + +/// Outcome of parsing an email address into its parts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedEmail { + pub local_part: String, + pub domain: String, +} + +/// Validate the syntax of an email address and split it into local-part and +/// domain. Returns `None` if the address is not syntactically valid. +pub fn parse_email(email: &str) -> Option { + let email = email.trim(); + + // Exactly one '@', and it must not be at either end. + let at = email.find('@')?; + if email[at + 1..].contains('@') { + return None; + } + let local = &email[..at]; + let domain = &email[at + 1..]; + + if !is_valid_local_part(local) || !is_valid_domain(domain) { + return None; + } + + Some(ParsedEmail { + local_part: local.to_string(), + domain: domain.to_string(), + }) +} + +/// Validate the local-part (the bit before `@`). Total address length and +/// local-part length limits come from RFC 5321 §4.5.3.1. +fn is_valid_local_part(local: &str) -> bool { + if local.is_empty() || local.len() > 64 { + return false; + } + // Dot-atom: cannot start/end with a dot, no consecutive dots. + if local.starts_with('.') || local.ends_with('.') || local.contains("..") { + return false; + } + // Permitted unquoted local-part characters (RFC 5322 atext + '.'). + local.chars().all(|c| { + c.is_ascii_alphanumeric() + || matches!( + c, + '.' | '!' + | '#' + | '$' + | '%' + | '&' + | '\'' + | '*' + | '+' + | '-' + | '/' + | '=' + | '?' + | '^' + | '_' + | '`' + | '{' + | '|' + | '}' + | '~' + ) + }) +} + +/// Validate the domain part. We accept conventional DNS host names; an MX +/// lookup later decides whether the domain actually accepts mail. +fn is_valid_domain(domain: &str) -> bool { + if domain.is_empty() || domain.len() > 253 { + return false; + } + // A deliverable domain has at least one dot (TLD); reject bare hostnames. + if !domain.contains('.') || domain.starts_with('.') || domain.ends_with('.') { + return false; + } + domain.split('.').all(is_valid_label) +} + +fn is_valid_label(label: &str) -> bool { + if label.is_empty() || label.len() > 63 { + return false; + } + if label.starts_with('-') || label.ends_with('-') { + return false; + } + label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') +} + +/// Common typo domains mapped to their intended correction. Used to offer a +/// "did you mean …" suggestion when the address is otherwise valid. +const DOMAIN_TYPOS: &[(&str, &str)] = &[ + ("gmial.com", "gmail.com"), + ("gmai.com", "gmail.com"), + ("gmal.com", "gmail.com"), + ("gnail.com", "gmail.com"), + ("gmail.co", "gmail.com"), + ("gmail.cm", "gmail.com"), + ("hotmial.com", "hotmail.com"), + ("hotmai.com", "hotmail.com"), + ("hotmal.com", "hotmail.com"), + ("hotmail.co", "hotmail.com"), + ("yaho.com", "yahoo.com"), + ("yahooo.com", "yahoo.com"), + ("yahoo.co", "yahoo.com"), + ("outlok.com", "outlook.com"), + ("outloo.com", "outlook.com"), + ("iclould.com", "icloud.com"), + ("icloud.co", "icloud.com"), +]; + +/// Suggest a corrected address if the domain looks like a known typo. +pub fn suggest_correction(parsed: &ParsedEmail) -> Option { + let domain_lower = parsed.domain.to_ascii_lowercase(); + DOMAIN_TYPOS + .iter() + .find(|(typo, _)| *typo == domain_lower) + .map(|(_, correct)| format!("{}@{}", parsed.local_part, correct)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_addresses() { + for email in [ + "test@example.com", + "user.name@example.com", + "user+tag@sub.example.co.uk", + "first.last@example.com", + "x@y.io", + "user_name@example-domain.com", + ] { + assert!(parse_email(email).is_some(), "{email} should be valid"); + } + } + + #[test] + fn test_invalid_addresses() { + for email in [ + "", + "plainaddress", + "@example.com", + "user@", + "user@@example.com", + "user@nodot", + ".user@example.com", + "user.@example.com", + "user..name@example.com", + "user@example..com", + "user@-example.com", + "user@example-.com", + "user name@example.com", + ] { + assert!(parse_email(email).is_none(), "{email} should be invalid"); + } + } + + #[test] + fn test_parts_extracted() { + let p = parse_email("alice.smith@mail.example.com").unwrap(); + assert_eq!(p.local_part, "alice.smith"); + assert_eq!(p.domain, "mail.example.com"); + } + + #[test] + fn test_trims_whitespace() { + assert!(parse_email(" test@example.com ").is_some()); + } + + #[test] + fn test_typo_suggestion() { + let p = parse_email("john@gmial.com").unwrap(); + assert_eq!(suggest_correction(&p), Some("john@gmail.com".to_string())); + + let ok = parse_email("john@gmail.com").unwrap(); + assert_eq!(suggest_correction(&ok), None); + } + + #[test] + fn test_local_part_length_limit() { + let long_local = "a".repeat(65); + assert!(parse_email(&format!("{long_local}@example.com")).is_none()); + let ok_local = "a".repeat(64); + assert!(parse_email(&format!("{ok_local}@example.com")).is_some()); + } +} diff --git a/crates/temps-email/src/services/validation_service.rs b/crates/temps-email/src/services/validation_service.rs deleted file mode 100644 index 133e3be8..00000000 --- a/crates/temps-email/src/services/validation_service.rs +++ /dev/null @@ -1,533 +0,0 @@ -//! Email validation service using check-if-email-exists library -//! -//! This service provides email validation capabilities to check if an email -//! address exists without sending any email. - -use check_if_email_exists::{ - check_email, CheckEmailInputBuilder, CheckEmailInputProxy, CheckEmailOutput, Reachable, -}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use tracing::{debug, info}; - -use crate::errors::EmailError; - -/// Configuration for email validation -#[derive(Debug, Clone, Default)] -pub struct ValidationConfig { - /// SOCKS5 proxy configuration for validation requests - pub proxy: Option, - /// From email address to use for SMTP validation - pub from_email: Option, - /// Hello name for SMTP HELO/EHLO command - pub hello_name: Option, -} - -/// Proxy configuration for email validation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProxyConfig { - pub host: String, - pub port: u16, - pub username: Option, - pub password: Option, -} - -/// Service for validating email addresses -pub struct ValidationService { - /// Configuration for email validation (proxy, from_email, hello_name). - /// Currently stored for future use with VerifMethod configuration. - #[allow(dead_code)] - config: ValidationConfig, -} - -/// Request to validate an email address -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ValidateEmailRequest { - /// Email address to validate - pub email: String, - /// Optional SOCKS5 proxy to use for this request - pub proxy: Option, -} - -/// Email reachability status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ReachabilityStatus { - /// Email is safe to send to - Safe, - /// Email might bounce, proceed with caution - Risky, - /// Email is invalid and will definitely bounce - Invalid, - /// Unable to determine deliverability - Unknown, -} - -impl From for ReachabilityStatus { - fn from(reachable: Reachable) -> Self { - match reachable { - Reachable::Safe => ReachabilityStatus::Safe, - Reachable::Risky => ReachabilityStatus::Risky, - Reachable::Invalid => ReachabilityStatus::Invalid, - Reachable::Unknown => ReachabilityStatus::Unknown, - } - } -} - -/// Syntax validation result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SyntaxResult { - /// Whether the email syntax is valid - pub is_valid_syntax: bool, - /// The domain part of the email - pub domain: Option, - /// The username part of the email - pub username: Option, - /// Suggested email correction if available - pub suggestion: Option, -} - -/// MX (Mail Exchange) validation result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MxResult { - /// Whether the domain accepts mail - pub accepts_mail: bool, - /// List of MX records for the domain - pub records: Vec, - /// Error message if MX lookup failed - pub error: Option, -} - -/// Misc validation result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MiscResult { - /// Whether the email is from a disposable email provider - pub is_disposable: bool, - /// Whether the email is a role-based account (e.g., admin@, info@) - pub is_role_account: bool, - /// Whether the email provider is a B2C (consumer) email provider - pub is_b2c: bool, - /// Gravatar URL if available - pub gravatar_url: Option, -} - -/// SMTP validation result -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SmtpResult { - /// Whether we could connect to the SMTP server - pub can_connect_smtp: bool, - /// Whether the mailbox appears to have a full inbox - pub has_full_inbox: bool, - /// Whether this is a catch-all domain - pub is_catch_all: bool, - /// Whether the email is deliverable - pub is_deliverable: bool, - /// Whether the mailbox is disabled - pub is_disabled: bool, - /// Error message if SMTP check failed - pub error: Option, -} - -/// Complete email validation response -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ValidateEmailResponse { - /// The email address that was validated - pub email: String, - /// Overall reachability status - pub is_reachable: ReachabilityStatus, - /// Syntax validation result - pub syntax: SyntaxResult, - /// MX record validation result - pub mx: MxResult, - /// Miscellaneous validation result - pub misc: MiscResult, - /// SMTP validation result - pub smtp: SmtpResult, -} - -impl From for ValidateEmailResponse { - fn from(output: CheckEmailOutput) -> Self { - // Extract syntax result - let syntax = SyntaxResult { - is_valid_syntax: output.syntax.is_valid_syntax, - domain: Some(output.syntax.domain.to_string()), - username: Some(output.syntax.username.to_string()), - suggestion: output.syntax.suggestion.clone(), - }; - - // Extract MX result - let mx = match &output.mx { - Ok(mx_details) => { - // MxDetails.lookup is Result - // When Ok, iterate over MxLookup using .iter() to get MX records - let records: Vec = mx_details - .lookup - .as_ref() - .map(|lookup| { - lookup - .iter() - .map(|host| host.exchange().to_string()) - .collect::>() - }) - .unwrap_or_else(|_| Vec::new()); - let accepts_mail = !records.is_empty(); - MxResult { - accepts_mail, - records, - error: None, - } - } - Err(e) => MxResult { - accepts_mail: false, - records: Vec::new(), - error: Some(format!("{:?}", e)), - }, - }; - - // Extract misc result - let misc = match &output.misc { - Ok(misc_details) => MiscResult { - is_disposable: misc_details.is_disposable, - is_role_account: misc_details.is_role_account, - is_b2c: misc_details.is_b2c, - gravatar_url: misc_details.gravatar_url.clone(), - }, - Err(_) => MiscResult { - is_disposable: false, - is_role_account: false, - is_b2c: false, - gravatar_url: None, - }, - }; - - // Extract SMTP result - let smtp = match &output.smtp { - Ok(smtp_details) => SmtpResult { - can_connect_smtp: smtp_details.can_connect_smtp, - has_full_inbox: smtp_details.has_full_inbox, - is_catch_all: smtp_details.is_catch_all, - is_deliverable: smtp_details.is_deliverable, - is_disabled: smtp_details.is_disabled, - error: None, - }, - Err(e) => SmtpResult { - can_connect_smtp: false, - has_full_inbox: false, - is_catch_all: false, - is_deliverable: false, - is_disabled: false, - error: Some(format!("{:?}", e)), - }, - }; - - ValidateEmailResponse { - email: output.input, - is_reachable: output.is_reachable.into(), - syntax, - mx, - misc, - smtp, - } - } -} - -impl ValidationService { - /// Create a new validation service with the given configuration - pub fn new(config: ValidationConfig) -> Self { - Self { config } - } - - /// Create a new validation service with default configuration - pub fn with_default_config() -> Self { - Self { - config: ValidationConfig::default(), - } - } - - /// Validate a single email address - pub async fn validate( - &self, - request: ValidateEmailRequest, - ) -> Result { - info!("Validating email: {}", request.email); - - use check_if_email_exists::smtp::verif_method::VerifMethod; - - let mut builder = CheckEmailInputBuilder::default(); - builder.to_email(request.email.clone()); - - let from_email = self - .config - .from_email - .clone() - .unwrap_or_else(|| "noreply@temps.sh".to_string()); - let hello_name = self - .config - .hello_name - .clone() - .unwrap_or_else(|| "temps.sh".to_string()); - - // Build proxy input if provided - let proxy_input = request.proxy.as_ref().map(|p| CheckEmailInputProxy { - host: p.host.clone(), - port: p.port, - username: p.username.clone(), - password: p.password.clone(), - timeout_ms: Some(10_000), - }); - - // Always set a VerifMethod with SMTP timeout to avoid hanging - let verif_method = VerifMethod::new_with_same_config_for_all( - proxy_input, - hello_name, - from_email, - 25, - Some(Duration::from_secs(10)), - 1, - ); - builder.verif_method(verif_method); - - let input = builder - .build() - .map_err(|e| EmailError::Validation(format!("Failed to build email input: {}", e)))?; - - debug!("Calling check_email for: {}", request.email); - - // Outer timeout as a safety net - let output = tokio::time::timeout(Duration::from_secs(20), async { - // Catch panics from the library (e.g. duplicate rustls crypto provider install) - let result = - tokio::task::spawn(async move { check_email(&input).await }).await; - result.map_err(|e| { - EmailError::Validation(format!( - "Email validation failed for internal error: {}", - e - )) - }) - }) - .await - .map_err(|_| { - EmailError::Validation(format!( - "Email validation timed out for {}. SMTP port 25 may be blocked — consider using a SOCKS5 proxy.", - request.email - )) - })??; - - debug!( - "Email validation result for {}: is_reachable={:?}", - request.email, output.is_reachable - ); - - Ok(ValidateEmailResponse::from(output)) - } - - /// Validate multiple email addresses - pub async fn validate_batch( - &self, - emails: Vec, - ) -> Result, EmailError> { - let mut results = Vec::with_capacity(emails.len()); - - for email in emails { - let request = ValidateEmailRequest { email, proxy: None }; - let result = self.validate(request).await?; - results.push(result); - } - - Ok(results) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_reachability_status_from_reachable() { - assert_eq!( - ReachabilityStatus::from(Reachable::Safe), - ReachabilityStatus::Safe - ); - assert_eq!( - ReachabilityStatus::from(Reachable::Risky), - ReachabilityStatus::Risky - ); - assert_eq!( - ReachabilityStatus::from(Reachable::Invalid), - ReachabilityStatus::Invalid - ); - assert_eq!( - ReachabilityStatus::from(Reachable::Unknown), - ReachabilityStatus::Unknown - ); - } - - #[test] - fn test_validation_config_default() { - let config = ValidationConfig::default(); - assert!(config.proxy.is_none()); - assert!(config.from_email.is_none()); - assert!(config.hello_name.is_none()); - } - - #[test] - fn test_validation_config_with_proxy() { - let config = ValidationConfig { - proxy: Some(ProxyConfig { - host: "proxy.example.com".to_string(), - port: 1080, - username: Some("user".to_string()), - password: Some("pass".to_string()), - }), - from_email: Some("test@example.com".to_string()), - hello_name: Some("mail.example.com".to_string()), - }; - - assert!(config.proxy.is_some()); - let proxy = config.proxy.unwrap(); - assert_eq!(proxy.host, "proxy.example.com"); - assert_eq!(proxy.port, 1080); - assert_eq!(proxy.username, Some("user".to_string())); - assert_eq!(proxy.password, Some("pass".to_string())); - } - - #[test] - fn test_validate_email_request() { - let request = ValidateEmailRequest { - email: "test@example.com".to_string(), - proxy: None, - }; - - assert_eq!(request.email, "test@example.com"); - assert!(request.proxy.is_none()); - } - - #[test] - fn test_syntax_result() { - let syntax = SyntaxResult { - is_valid_syntax: true, - domain: Some("example.com".to_string()), - username: Some("test".to_string()), - suggestion: None, - }; - - assert!(syntax.is_valid_syntax); - assert_eq!(syntax.domain, Some("example.com".to_string())); - assert_eq!(syntax.username, Some("test".to_string())); - assert!(syntax.suggestion.is_none()); - } - - #[test] - fn test_mx_result() { - let mx = MxResult { - accepts_mail: true, - records: vec!["mx1.example.com".to_string(), "mx2.example.com".to_string()], - error: None, - }; - - assert!(mx.accepts_mail); - assert_eq!(mx.records.len(), 2); - assert!(mx.error.is_none()); - } - - #[test] - fn test_misc_result() { - let misc = MiscResult { - is_disposable: false, - is_role_account: true, - is_b2c: false, - gravatar_url: Some("https://gravatar.com/avatar/xxx".to_string()), - }; - - assert!(!misc.is_disposable); - assert!(misc.is_role_account); - assert!(!misc.is_b2c); - assert!(misc.gravatar_url.is_some()); - } - - #[test] - fn test_smtp_result() { - let smtp = SmtpResult { - can_connect_smtp: true, - has_full_inbox: false, - is_catch_all: false, - is_deliverable: true, - is_disabled: false, - error: None, - }; - - assert!(smtp.can_connect_smtp); - assert!(!smtp.has_full_inbox); - assert!(!smtp.is_catch_all); - assert!(smtp.is_deliverable); - assert!(!smtp.is_disabled); - assert!(smtp.error.is_none()); - } - - #[test] - fn test_validate_email_response() { - let response = ValidateEmailResponse { - email: "test@example.com".to_string(), - is_reachable: ReachabilityStatus::Safe, - syntax: SyntaxResult { - is_valid_syntax: true, - domain: Some("example.com".to_string()), - username: Some("test".to_string()), - suggestion: None, - }, - mx: MxResult { - accepts_mail: true, - records: vec!["mx.example.com".to_string()], - error: None, - }, - misc: MiscResult { - is_disposable: false, - is_role_account: false, - is_b2c: false, - gravatar_url: None, - }, - smtp: SmtpResult { - can_connect_smtp: true, - has_full_inbox: false, - is_catch_all: false, - is_deliverable: true, - is_disabled: false, - error: None, - }, - }; - - assert_eq!(response.email, "test@example.com"); - assert_eq!(response.is_reachable, ReachabilityStatus::Safe); - assert!(response.syntax.is_valid_syntax); - assert!(response.mx.accepts_mail); - assert!(!response.misc.is_disposable); - assert!(response.smtp.is_deliverable); - } - - #[test] - fn test_validation_service_with_default_config() { - let service = ValidationService::with_default_config(); - assert!(service.config.proxy.is_none()); - assert!(service.config.from_email.is_none()); - assert!(service.config.hello_name.is_none()); - } - - #[test] - fn test_validation_service_with_config() { - let config = ValidationConfig { - proxy: None, - from_email: Some("validator@example.com".to_string()), - hello_name: Some("mail.example.com".to_string()), - }; - - let service = ValidationService::new(config); - assert_eq!( - service.config.from_email, - Some("validator@example.com".to_string()) - ); - assert_eq!( - service.config.hello_name, - Some("mail.example.com".to_string()) - ); - } -} diff --git a/crates/temps-entities/src/backup_schedule_services.rs b/crates/temps-entities/src/backup_schedule_services.rs new file mode 100644 index 00000000..09fbe545 --- /dev/null +++ b/crates/temps-entities/src/backup_schedule_services.rs @@ -0,0 +1,48 @@ +//! Join table linking a backup schedule to the external services it targets. +//! +//! See migration `m20260519_000001_create_backup_schedule_services` for the +//! schema rationale. + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use temps_core::DBDateTime; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "backup_schedule_services")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub schedule_id: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub service_id: i32, + pub created_at: DBDateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::backup_schedules::Entity", + from = "Column::ScheduleId", + to = "super::backup_schedules::Column::Id" + )] + Schedule, + #[sea_orm( + belongs_to = "super::external_services::Entity", + from = "Column::ServiceId", + to = "super::external_services::Column::Id" + )] + Service, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Schedule.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Service.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/temps-entities/src/backup_schedules.rs b/crates/temps-entities/src/backup_schedules.rs index 6b68db5e..b1af4746 100644 --- a/crates/temps-entities/src/backup_schedules.rs +++ b/crates/temps-entities/src/backup_schedules.rs @@ -25,6 +25,18 @@ pub struct Model { /// /// `None` means "use the engine default." pub max_runtime_secs: Option, + /// When `true`, fan-out targets every external service on the host + /// (auto-including future databases). When `false`, fan-out targets + /// only the services attached via `backup_schedule_services`. Default + /// is `true` so a fresh schedule "just backs up everything." + #[sea_orm(default_value = true)] + pub target_all_services: bool, + /// When `true`, every run also produces a `control_plane` backup + /// (Temps's own Postgres). When `false`, only the external service + /// fan-out happens — useful when the operator scopes a schedule to a + /// single DB and doesn't want the control plane lumped in. + #[sea_orm(default_value = true)] + pub include_control_plane: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -35,6 +47,8 @@ pub enum Relation { to = "super::s3_sources::Column::Id" )] S3Source, + #[sea_orm(has_many = "super::backup_schedule_services::Entity")] + Services, } impl Related for Entity { @@ -43,6 +57,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Services.def() + } +} + #[async_trait] impl ActiveModelBehavior for ActiveModel { async fn before_save(mut self, _db: &C, insert: bool) -> Result diff --git a/crates/temps-entities/src/external_services.rs b/crates/temps-entities/src/external_services.rs index 1fb6856a..dd815db1 100644 --- a/crates/temps-entities/src/external_services.rs +++ b/crates/temps-entities/src/external_services.rs @@ -49,6 +49,8 @@ pub enum Relation { ProjectServices, #[sea_orm(has_many = "super::service_members::Entity")] Members, + #[sea_orm(has_many = "super::backup_schedule_services::Entity")] + BackupScheduleServices, #[sea_orm( belongs_to = "super::nodes::Entity", from = "Column::NodeId", @@ -75,6 +77,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::BackupScheduleServices.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Node.def() diff --git a/crates/temps-entities/src/lib.rs b/crates/temps-entities/src/lib.rs index dff57aef..d65396c3 100644 --- a/crates/temps-entities/src/lib.rs +++ b/crates/temps-entities/src/lib.rs @@ -19,6 +19,7 @@ pub mod autopilot_configs; pub mod autopilot_run_logs; pub mod autopilot_runs; pub mod backup_alerts; +pub mod backup_schedule_services; pub mod backup_schedules; pub mod backups; pub mod challenge_sessions; diff --git a/crates/temps-import-docker/src/importer.rs b/crates/temps-import-docker/src/importer.rs index 0e2011d8..0320f0be 100644 --- a/crates/temps-import-docker/src/importer.rs +++ b/crates/temps-import-docker/src/importer.rs @@ -2,8 +2,9 @@ use async_trait::async_trait; use bollard::{ - models::ContainerInspectResponse, query_parameters::ListContainersOptions, - secret::RestartPolicyNameEnum, Docker, + models::{ContainerInspectResponse, RestartPolicyNameEnum}, + query_parameters::ListContainersOptions, + Docker, }; use std::{collections::HashMap, sync::Arc}; use temps_import_types::{ diff --git a/crates/temps-infra/Cargo.toml b/crates/temps-infra/Cargo.toml index 4b0a3059..f5f9a854 100644 --- a/crates/temps-infra/Cargo.toml +++ b/crates/temps-infra/Cargo.toml @@ -22,7 +22,7 @@ anyhow = { workspace = true } # Crate-specific dependencies get_if_addrs = "0.5" parking_lot = "0.12" -hickory-resolver = "0.24" +hickory-resolver = "0.26" # Plugin system dependencies temps-core = { path = "../temps-core" } diff --git a/crates/temps-infra/src/services/dns.rs b/crates/temps-infra/src/services/dns.rs index 40b90cdc..2abe64a9 100644 --- a/crates/temps-infra/src/services/dns.rs +++ b/crates/temps-infra/src/services/dns.rs @@ -1,6 +1,7 @@ use anyhow::Result; use hickory_resolver::config::*; -use hickory_resolver::TokioAsyncResolver; +use hickory_resolver::net::runtime::TokioRuntimeProvider; +use hickory_resolver::Resolver; /// Result of a DNS A record lookup #[derive(Debug, Clone)] @@ -28,21 +29,25 @@ impl DnsService { } /// Create a fresh resolver with no caching - async fn create_resolver(&self) -> Result<(TokioAsyncResolver, Vec)> { + async fn create_resolver(&self) -> Result<(Resolver, Vec)> { let config = ResolverConfig::default(); let mut opts = ResolverOpts::default(); // Disable caching to get fresh data opts.cache_size = 0; - opts.use_hosts_file = false; + opts.use_hosts_file = ResolveHosts::Never; - let resolver = TokioAsyncResolver::tokio(config.clone(), opts); + let resolver = + Resolver::builder_with_config(config.clone(), TokioRuntimeProvider::default()) + .with_options(opts) + .build() + .map_err(|e| anyhow::anyhow!("failed to build DNS resolver: {}", e))?; - // Extract DNS server addresses + // Extract DNS server addresses (hickory 0.26: NameServerConfig.ip). let dns_servers: Vec = config .name_servers() .iter() - .map(|ns| ns.socket_addr.ip().to_string()) + .map(|ns| ns.ip.to_string()) .collect(); Ok((resolver, dns_servers)) @@ -50,15 +55,26 @@ impl DnsService { /// Lookup A records for a domain name with fresh data pub async fn lookup_a_records(&self, domain: &str) -> Result { + use hickory_resolver::proto::rr::{RData, RecordType}; + // Create a fresh resolver for each lookup (no caching) let (resolver, dns_servers) = self.create_resolver().await?; + // Generic `lookup` returns a `Lookup`; pull the A rdata out of each + // answer record (hickory 0.26 — record.data is the typed RData). let response = resolver - .ipv4_lookup(domain) + .lookup(domain, RecordType::A) .await .map_err(|e| anyhow::anyhow!("DNS lookup failed: {}", e))?; - let records: Vec = response.iter().map(|ip| ip.to_string()).collect(); + let records: Vec = response + .answers() + .iter() + .filter_map(|record| match &record.data { + RData::A(a) => Some(a.0.to_string()), + _ => None, + }) + .collect(); Ok(DnsLookupResult { records, diff --git a/crates/temps-mcp/Cargo.toml b/crates/temps-mcp/Cargo.toml deleted file mode 100644 index 05405936..00000000 --- a/crates/temps-mcp/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "temps-mcp" -version.workspace = true -edition.workspace = true -license.workspace = true -authors.workspace = true -repository.workspace = true -homepage.workspace = true - -[dependencies] -temps-auth = { path = "../temps-auth" } -temps-core = { path = "../temps-core" } -temps-config = { path = "../temps-config" } -temps-database = { path = "../temps-database" } -temps-domains = { path = "../temps-domains" } -temps-entities = { path = "../temps-entities" } -temps-projects = { path = "../temps-projects" } - -serde = { workspace = true } -tokio = { workspace = true } -rmcp = { version = "0.6.1", features = [ - "server", - "macros", - "transport-sse-server", - "transport-io", - "transport-streamable-http-server", - "auth", - "elicitation", - "schemars", -] } -tracing = { workspace = true } -serde_json = { workspace = true } -anyhow = { workspace = true } diff --git a/crates/temps-mcp/src/lib.rs b/crates/temps-mcp/src/lib.rs deleted file mode 100644 index 335882e1..00000000 --- a/crates/temps-mcp/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! mcp services and utilities - -pub mod mcp; - -pub use mcp::*; diff --git a/crates/temps-mcp/src/mcp.rs b/crates/temps-mcp/src/mcp.rs deleted file mode 100644 index 88c5acc3..00000000 --- a/crates/temps-mcp/src/mcp.rs +++ /dev/null @@ -1,554 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{debug, error, info}; - -use rmcp::{ - handler::server::{ - router::{prompt::PromptRouter, tool::ToolRouter}, - wrapper::Parameters, - }, - model::*, - prompt, prompt_handler, prompt_router, schemars, - service::RequestContext, - tool, tool_handler, tool_router, ErrorData as McpError, RoleServer, ServerHandler, -}; - -// Import project service from temps-projects crate -use temps_projects::services::project::ProjectService; -use temps_projects::services::types::ProjectError; - -#[derive(Clone)] -pub struct McpService { - clients: Arc>>, - prompts: Arc>>, - resources: Arc>>, - project_service: Option>, - tool_router: ToolRouter, - prompt_router: PromptRouter, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct McpClient { - pub id: String, - pub name: String, - pub command: String, - pub args: Vec, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct McpPrompt { - pub id: String, - pub name: String, - pub description: String, - pub arguments: Vec, - pub template: String, - pub client_id: Option, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct McpArgument { - pub name: String, - pub description: String, - pub required: bool, - pub argument_type: String, -} - -// Define request/response structures for tools and prompts -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct ProjectInfoArgs { - /// The slug of the project to get information about - pub project_slug: String, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct McpResource { - pub id: String, - pub uri: String, - pub name: String, - pub description: String, - pub mime_type: Option, - pub client_id: Option, -} - -#[tool_router] -impl McpService { - pub fn new() -> Self { - let prompts = Vec::new(); - let resources = Vec::new(); // Will be populated dynamically - - Self { - clients: Arc::new(RwLock::new(Vec::new())), - prompts: Arc::new(RwLock::new(prompts)), - resources: Arc::new(RwLock::new(resources)), - project_service: None, - tool_router: Self::tool_router(), - prompt_router: Self::prompt_router(), - } - } - - // Tool implementations - #[tool(description = "List all available projects")] - async fn list_projects(&self) -> Result { - if let Some(project_service) = &self.project_service { - match project_service.get_projects().await { - Ok(projects) => { - let projects_json = serde_json::to_string_pretty(&projects) - .unwrap_or_else(|_| "Failed to serialize projects".to_string()); - - Ok(CallToolResult::success(vec![Content::text(format!( - "Found {} projects:\n{}", - projects.len(), - projects_json - ))])) - } - Err(e) => { - error!("Failed to fetch projects: {}", e); - Err(McpError::internal_error( - "Failed to fetch projects", - Some(json!({"error": e.to_string()})), - )) - } - } - } else { - Err(McpError::internal_error( - "Project service not available", - None, - )) - } - } - - #[tool(description = "Get information about a specific project by slug")] - async fn get_project( - &self, - Parameters(args): Parameters, - ) -> Result { - if let Some(project_service) = &self.project_service { - match project_service - .get_project_by_slug(&args.project_slug) - .await - { - Ok(project) => { - let mut result = "Project Information:\n".to_string(); - result.push_str(&format!("ID: {}\n", project.id)); - result.push_str(&format!("Name: {}\n", project.name)); - result.push_str(&format!("Slug: {}\n", project.slug)); - result.push_str(&format!( - "Repository: {}/{}\n", - project.repo_owner.unwrap_or("unknown".to_string()), - project.repo_name.unwrap_or("unknown".to_string()) - )); - result.push_str(&format!("Directory: {}\n", project.directory)); - result.push_str(&format!("Branch: {}\n", project.main_branch)); - result.push_str(&format!("Auto Deploy: {}\n", project.automatic_deploy)); - result.push_str(&format!("Created: {}\n", project.created_at)); - result.push_str(&format!("Updated: {}\n", project.updated_at)); - - result.push_str( - "\nNote: Deployment information is not available in this service.\n", - ); - - Ok(CallToolResult::success(vec![Content::text(result)])) - } - Err(ProjectError::NotFound(_)) => { - Err(McpError::invalid_params("Project not found", None)) - } - Err(e) => { - error!("Failed to fetch project {}: {}", args.project_slug, e); - Err(McpError::internal_error( - "Failed to fetch project", - Some(json!({"error": e.to_string()})), - )) - } - } - } else { - Err(McpError::internal_error( - "Project service not available", - None, - )) - } - } - - pub fn with_project_service(mut self, project_service: Arc) -> Self { - self.project_service = Some(project_service); - self - } - - pub async fn initialize_mcp_server(&self) -> anyhow::Result<()> { - info!("Initializing MCP server with built-in prompts and resources"); - - // Populate resources dynamically - self.populate_resources().await?; - - info!( - "MCP server initialized with {} prompts and {} resources", - self.prompts.read().await.len(), - self.resources.read().await.len() - ); - Ok(()) - } - - async fn populate_resources(&self) -> anyhow::Result<()> { - let mut resources = self.resources.write().await; - resources.clear(); - - // Add general project listing resource - resources.push(McpResource { - id: "projects-resource".to_string(), - uri: "project://".to_string(), - name: "Projects".to_string(), - description: "Access to all project data and configurations".to_string(), - mime_type: Some("application/json".to_string()), - client_id: None, - }); - - // Add individual project resources if project service is available - if let Some(project_service) = &self.project_service { - match project_service.get_projects().await { - Ok(projects) => { - for project in projects { - resources.push(McpResource { - id: format!("project-{}", project.slug), - uri: format!("project://{}", project.slug), - name: format!("Project: {}", project.name), - description: format!( - "Access to {} project data and configurations", - project.name - ), - mime_type: Some("application/json".to_string()), - client_id: None, - }); - } - } - Err(e) => { - error!("Failed to populate project resources: {}", e); - } - } - } - - Ok(()) - } - - pub async fn add_client( - &self, - id: String, - name: String, - command: String, - args: Vec, - ) -> anyhow::Result<()> { - let client = McpClient { - id, - name: name.clone(), - command, - args, - }; - let mut clients = self.clients.write().await; - clients.push(client); - info!("Added MCP client: {}", name); - Ok(()) - } - - pub async fn list_clients(&self) -> Vec { - let clients = self.clients.read().await; - clients.clone() - } - - pub async fn remove_client(&self, id: &str) -> anyhow::Result { - let mut clients = self.clients.write().await; - let initial_len = clients.len(); - clients.retain(|client| client.id != id); - let removed = clients.len() != initial_len; - if removed { - info!("Removed MCP client with id: {}", id); - } - Ok(removed) - } - - pub async fn connect_to_client(&self, id: &str) -> anyhow::Result { - let clients = self.clients.read().await; - let client = clients - .iter() - .find(|c| c.id == id) - .ok_or_else(|| anyhow::anyhow!("Client not found"))?; - - debug!("Connecting to MCP client: {}", client.name); - - // For now, we'll return a mock response - // In production, this would establish actual MCP client connections - info!("Mock connection to MCP client: {}", client.name); - - Ok(serde_json::json!({ - "status": "connected", - "client_id": id, - "client_name": client.name - })) - } - - pub async fn execute_tool( - &self, - client_id: &str, - tool_name: &str, - arguments: Value, - ) -> anyhow::Result { - debug!("Executing tool {} on client {}", tool_name, client_id); - - Ok(serde_json::json!({ - "result": "Tool execution not yet implemented", - "tool": tool_name, - "client": client_id, - "arguments": arguments - })) - } - - // Prompt management methods - pub async fn list_prompts(&self) -> Vec { - let prompts = self.prompts.read().await; - prompts.clone() - } - - pub async fn get_prompt(&self, id: &str) -> anyhow::Result { - let prompts = self.prompts.read().await; - prompts - .iter() - .find(|p| p.id == id) - .cloned() - .ok_or_else(|| anyhow::anyhow!("Prompt not found")) - } - - pub async fn execute_prompt( - &self, - id: &str, - arguments: HashMap, - ) -> anyhow::Result { - let prompt = self.get_prompt(id).await?; - let mut result = prompt.template.clone(); - - for (key, value) in arguments { - result = result.replace(&format!("{{{{{}}}}}", key), &value); - } - - debug!("Executed prompt {}: {}", id, result); - Ok(result) - } - - // Resource management methods - pub async fn list_resources(&self) -> Vec { - let resources = self.resources.read().await; - resources.clone() - } - - pub async fn get_resource(&self, uri: &str) -> anyhow::Result { - debug!("Fetching resource: {}", uri); - - if uri.starts_with("project://") { - return self.get_project_resource(uri).await; - } - - Err(anyhow::anyhow!("Unsupported resource URI: {}", uri)) - } - - async fn get_project_resource(&self, uri: &str) -> anyhow::Result { - let project_service = self - .project_service - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Project service not available"))?; - - let path = uri.strip_prefix("project://").unwrap_or(""); - - if path.is_empty() { - // Return list of all projects - match project_service.get_projects().await { - Ok(projects) => Ok(serde_json::json!({ - "type": "project_list", - "uri": uri, - "data": { - "projects": projects, - "count": projects.len() - } - })), - Err(e) => Err(anyhow::anyhow!("Failed to fetch projects: {}", e)), - } - } else { - // Try to get project by slug first (preferred) - match project_service.get_project_by_slug(path).await { - Ok(project) => Ok(serde_json::json!({ - "type": "project_detail", - "uri": uri, - "data": { - "project": project - } - })), - Err(_) => { - // If slug lookup fails, try ID as fallback (for backward compatibility) - if let Ok(project_id) = path.parse::() { - match project_service.get_project(project_id).await { - Ok(project) => Ok(serde_json::json!({ - "type": "project_detail", - "uri": uri, - "data": { - "project": project - } - })), - Err(e) => Err(anyhow::anyhow!( - "Failed to fetch project '{}' by slug or ID: {}", - path, - e - )), - } - } else { - Err(anyhow::anyhow!("Project '{}' not found by slug", path)) - } - } - } - } - } - - pub async fn add_resource(&self, resource: McpResource) -> anyhow::Result<()> { - let mut resources = self.resources.write().await; - resources.push(resource); - Ok(()) - } - - pub async fn remove_resource(&self, id: &str) -> anyhow::Result { - let mut resources = self.resources.write().await; - let initial_len = resources.len(); - resources.retain(|r| r.id != id); - Ok(resources.len() != initial_len) - } -} - -// Prompt router implementation -#[prompt_router] -impl McpService { - /// Get detailed information about a specific project - #[prompt(name = "project_info")] - async fn project_info_prompt( - &self, - Parameters(args): Parameters, - _ctx: RequestContext, - ) -> Result { - let messages = vec![ - PromptMessage::new_text( - PromptMessageRole::Assistant, - "You are a helpful assistant that provides information about projects.", - ), - PromptMessage::new_text( - PromptMessageRole::User, - format!( - "Please provide detailed information about project with slug: {}\n\ - Include:\n\ - - Project configuration\n\ - - Current deployment status\n\ - - Recent pipeline runs\n\ - - Associated domains\n\ - - Environment variables", - args.project_slug - ), - ), - ]; - - Ok(GetPromptResult { - description: Some(format!( - "Get information about project {}", - args.project_slug - )), - messages, - }) - } -} - -// ServerHandler implementation for MCP protocol -#[tool_handler] -#[prompt_handler] -impl ServerHandler for McpService { - fn get_info(&self) -> ServerInfo { - ServerInfo { - protocol_version: ProtocolVersion::V_2024_11_05, - capabilities: ServerCapabilities::builder() - .enable_prompts() - .enable_resources() - .enable_tools() - .build(), - server_info: Implementation { - name: "indie-hacker-engine-mcp".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - title: Some("Indie Hacker Engine MCP Server".to_string()), - website_url: None, - icons: None, - }, - instructions: Some( - "This MCP server provides access to project data from the Indie Hacker Engine platform. \ - Available tools: list_projects, get_project (uses project slug). \ - Available prompts: project_info (uses project slug for detailed information). \ - Available resources: project:// (access project data by slug/ID). \ - Slugs are preferred over IDs for better usability." - .to_string() - ), - } - } - - async fn list_resources( - &self, - _request: Option, - _: RequestContext, - ) -> Result { - let resources = self.resources.read().await; - let raw_resources = resources - .iter() - .map(|resource| RawResource::new(&resource.uri, resource.name.clone()).no_annotation()) - .collect(); - - Ok(ListResourcesResult { - resources: raw_resources, - next_cursor: None, - }) - } - - async fn read_resource( - &self, - ReadResourceRequestParam { uri }: ReadResourceRequestParam, - _: RequestContext, - ) -> Result { - match self.get_resource(&uri).await { - Ok(data) => Ok(ReadResourceResult { - contents: vec![ResourceContents::text( - serde_json::to_string_pretty(&data) - .unwrap_or_else(|_| "Error serializing data".to_string()), - uri, - )], - }), - Err(_) => Err(McpError::resource_not_found( - "Resource not found", - Some(json!({ "uri": uri })), - )), - } - } - - async fn list_resource_templates( - &self, - _request: Option, - _: RequestContext, - ) -> Result { - Ok(ListResourceTemplatesResult { - next_cursor: None, - resource_templates: Vec::new(), - }) - } - - async fn initialize( - &self, - _request: InitializeRequestParam, - _context: RequestContext, - ) -> Result { - info!("MCP server initialized"); - Ok(self.get_info()) - } -} - -impl Default for McpService { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/temps-migrations/src/migration/m20260519_000001_create_backup_schedule_services.rs b/crates/temps-migrations/src/migration/m20260519_000001_create_backup_schedule_services.rs new file mode 100644 index 00000000..e138e3ee --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260519_000001_create_backup_schedule_services.rs @@ -0,0 +1,119 @@ +//! Migration: create `backup_schedule_services` join table. +//! +//! ## Purpose +//! +//! Lets a backup schedule target one or more external services explicitly. +//! Before this table, [`enqueue_scheduled_run`] fanned out to *every* external +//! service the host knew about — users had no way to say "this schedule backs +//! up these databases." +//! +//! ## Behaviour change on upgrade +//! +//! This migration intentionally **does not** backfill existing schedules. +//! Existing schedules will produce only the control-plane backup until users +//! attach services via `POST /api/backups/schedules/{id}/services` (or the UI). +//! This is the deliberate fix for the "schedules silently back up every DB" +//! bug — the next operator action must be explicit. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + manager + .create_table( + Table::create() + .table(BackupScheduleServices::Table) + .if_not_exists() + .col( + ColumnDef::new(BackupScheduleServices::ScheduleId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(BackupScheduleServices::ServiceId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(BackupScheduleServices::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .primary_key( + Index::create() + .name("backup_schedule_services_pkey") + .col(BackupScheduleServices::ScheduleId) + .col(BackupScheduleServices::ServiceId), + ) + .foreign_key( + ForeignKey::create() + .name("fk_backup_schedule_services_schedule_id") + .from( + BackupScheduleServices::Table, + BackupScheduleServices::ScheduleId, + ) + .to(BackupSchedules::Table, BackupSchedules::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_backup_schedule_services_service_id") + .from( + BackupScheduleServices::Table, + BackupScheduleServices::ServiceId, + ) + .to(ExternalServices::Table, ExternalServices::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Reverse-lookup index: "which schedules back up this service?" + db.execute_unprepared( + "CREATE INDEX IF NOT EXISTS backup_schedule_services_service_id_idx \ + ON backup_schedule_services (service_id)", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table( + Table::drop() + .table(BackupScheduleServices::Table) + .to_owned(), + ) + .await?; + Ok(()) + } +} + +#[derive(DeriveIden)] +enum BackupScheduleServices { + Table, + ScheduleId, + ServiceId, + CreatedAt, +} + +#[derive(DeriveIden)] +enum BackupSchedules { + Table, + Id, +} + +#[derive(DeriveIden)] +enum ExternalServices { + Table, + Id, +} diff --git a/crates/temps-migrations/src/migration/m20260519_000002_add_target_all_services.rs b/crates/temps-migrations/src/migration/m20260519_000002_add_target_all_services.rs new file mode 100644 index 00000000..53a27834 --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260519_000002_add_target_all_services.rs @@ -0,0 +1,53 @@ +//! Migration: add `target_all_services` to `backup_schedules`. +//! +//! ## Purpose +//! +//! Restores the "back up every database" default in a controllable way. +//! After `m20260519_000001_create_backup_schedule_services`, schedules with +//! no attached rows produced only the control-plane backup — that's the +//! right behaviour when an operator explicitly scopes down, but a bad +//! default for "I just want all my DBs backed up forever, including future +//! ones." +//! +//! With this column: +//! - `target_all_services = true` → fan-out loads every external service +//! (auto-includes future databases). +//! - `target_all_services = false` → fan-out uses the explicit +//! `backup_schedule_services` membership table. +//! +//! ## Backfill +//! +//! Existing schedules backfill to `TRUE`. This is the safer default for +//! operators upgrading from the previous migration, which had effectively +//! disabled service backups for legacy schedules. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + "ALTER TABLE backup_schedules \ + ADD COLUMN IF NOT EXISTS target_all_services BOOLEAN NOT NULL DEFAULT TRUE", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + "ALTER TABLE backup_schedules DROP COLUMN IF EXISTS target_all_services", + ) + .await?; + + Ok(()) + } +} diff --git a/crates/temps-migrations/src/migration/m20260519_000003_add_include_control_plane.rs b/crates/temps-migrations/src/migration/m20260519_000003_add_include_control_plane.rs new file mode 100644 index 00000000..ff2a4013 --- /dev/null +++ b/crates/temps-migrations/src/migration/m20260519_000003_add_include_control_plane.rs @@ -0,0 +1,49 @@ +//! Migration: add `include_control_plane` to `backup_schedules`. +//! +//! ## Purpose +//! +//! Previously every scheduled run fanned out to *both* the Temps control +//! plane (its own Postgres) AND the selected external services. That made +//! sense for "back up everything" schedules but was always a forced +//! tax-along on schedules that the operator scoped to a specific database +//! list — the run history would show a `control_plane` backup row next to +//! every Postgres/Redis backup whether they wanted it or not. +//! +//! With this column the operator picks per-schedule whether the control +//! plane is in scope, independently of `target_all_services`. +//! +//! ## Backfill +//! +//! Existing rows default to `TRUE` so existing runs keep producing the +//! control-plane backup. Operators opt out by editing the schedule. + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + "ALTER TABLE backup_schedules \ + ADD COLUMN IF NOT EXISTS include_control_plane BOOLEAN NOT NULL DEFAULT TRUE", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + db.execute_unprepared( + "ALTER TABLE backup_schedules DROP COLUMN IF EXISTS include_control_plane", + ) + .await?; + + Ok(()) + } +} diff --git a/crates/temps-migrations/src/migration/mod.rs b/crates/temps-migrations/src/migration/mod.rs index 2c7f945c..358dcb9f 100644 --- a/crates/temps-migrations/src/migration/mod.rs +++ b/crates/temps-migrations/src/migration/mod.rs @@ -95,6 +95,9 @@ mod m20260516_000001_create_schedule_runs; mod m20260517_000001_add_health_metadata_to_external_services; mod m20260517_000002_drop_backup_jobs; mod m20260518_000001_drop_backups_last_heartbeat_at; +mod m20260519_000001_create_backup_schedule_services; +mod m20260519_000002_add_target_all_services; +mod m20260519_000003_add_include_control_plane; pub struct Migrator; @@ -193,6 +196,9 @@ impl MigratorTrait for Migrator { Box::new(m20260517_000001_add_health_metadata_to_external_services::Migration), Box::new(m20260517_000002_drop_backup_jobs::Migration), Box::new(m20260518_000001_drop_backups_last_heartbeat_at::Migration), + Box::new(m20260519_000001_create_backup_schedule_services::Migration), + Box::new(m20260519_000002_add_target_all_services::Migration), + Box::new(m20260519_000003_add_include_control_plane::Migration), ] } } diff --git a/crates/temps-notifications/src/digest/digest_service.rs b/crates/temps-notifications/src/digest/digest_service.rs index 98a5a5bb..f239ca59 100644 --- a/crates/temps-notifications/src/digest/digest_service.rs +++ b/crates/temps-notifications/src/digest/digest_service.rs @@ -10,7 +10,9 @@ use sea_orm::{ QuerySelect, }; use std::sync::Arc; -use temps_entities::{deployments, events, projects}; +use temps_entities::{ + deployments, error_events, error_groups, events, funnel_steps, funnels, projects, +}; use tracing::{error, info}; pub struct DigestService { @@ -157,21 +159,234 @@ impl DigestService { 0.0 }; - // TODO: Implement more detailed analytics queries - // For now, return basic data + let average_session_duration = self + .query_average_session_duration(week_start, week_end) + .await + .unwrap_or(0.0); + let bounce_rate = self + .query_bounce_rate(week_start, week_end) + .await + .unwrap_or(0.0); + let top_pages = self + .query_top_pages(week_start, week_end) + .await + .unwrap_or_default(); + let geographic_distribution = self + .query_geographic_distribution(week_start, week_end, total_visitors) + .await + .unwrap_or_default(); + let visitor_trend = self + .query_visitor_trend(week_start, week_end) + .await + .unwrap_or_default(); + Ok(PerformanceData { total_visitors, unique_sessions: total_visitors, page_views, - average_session_duration: 0.0, - bounce_rate: 0.0, - top_pages: vec![], - geographic_distribution: vec![], - visitor_trend: vec![], + average_session_duration, + bounce_rate, + top_pages, + geographic_distribution, + visitor_trend, week_over_week_change, }) } + /// Average session duration in minutes. A session's duration is the span + /// from its first to its last event; sessions are then averaged. + async fn query_average_session_duration( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT COALESCE(AVG(session_seconds), 0)::float8 AS avg_seconds + FROM ( + SELECT EXTRACT(EPOCH FROM (MAX(timestamp) - MIN(timestamp))) AS session_seconds + FROM events + WHERE timestamp BETWEEN $1 AND $2 + AND session_id IS NOT NULL + AND is_crawler = false + GROUP BY session_id + ) s + "#, + [week_start.into(), week_end.into()], + ); + + let avg_seconds: f64 = self + .db + .query_one(stmt) + .await? + .and_then(|row| row.try_get::("", "avg_seconds").ok()) + .unwrap_or(0.0); + + Ok(avg_seconds / 60.0) + } + + /// Bounce rate: percentage of sessions whose entry event is flagged as a + /// bounce (single-interaction session). + async fn query_bounce_rate( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + COUNT(*) FILTER (WHERE bounced)::float8 AS bounced, + COUNT(*)::float8 AS total + FROM ( + SELECT bool_or(is_bounce) AS bounced + FROM events + WHERE timestamp BETWEEN $1 AND $2 + AND session_id IS NOT NULL + AND is_crawler = false + GROUP BY session_id + ) s + "#, + [week_start.into(), week_end.into()], + ); + + if let Some(row) = self.db.query_one(stmt).await? { + let bounced: f64 = row.try_get("", "bounced").unwrap_or(0.0); + let total: f64 = row.try_get("", "total").unwrap_or(0.0); + if total > 0.0 { + return Ok((bounced / total) * 100.0); + } + } + Ok(0.0) + } + + /// Top pages by view count for the week (max 5). + async fn query_top_pages( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + page_path, + COUNT(*)::bigint AS views, + COUNT(DISTINCT session_id)::bigint AS unique_visitors + FROM events + WHERE timestamp BETWEEN $1 AND $2 + AND is_crawler = false + GROUP BY page_path + ORDER BY views DESC + LIMIT 5 + "#, + [week_start.into(), week_end.into()], + ); + + let rows = self.db.query_all(stmt).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + Some(TopPage { + path: row.try_get("", "page_path").ok()?, + views: row.try_get("", "views").ok()?, + unique_visitors: row.try_get("", "unique_visitors").ok()?, + }) + }) + .collect()) + } + + /// Top countries by visitor count for the week (max 5), with each + /// country's share of `total_visitors`. + async fn query_geographic_distribution( + &self, + week_start: DateTime, + week_end: DateTime, + total_visitors: i64, + ) -> Result> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + g.country AS country, + COUNT(DISTINCT e.session_id)::bigint AS visitors + FROM events e + JOIN ip_geolocations g ON g.id = e.ip_geolocation_id + WHERE e.timestamp BETWEEN $1 AND $2 + AND e.session_id IS NOT NULL + AND e.is_crawler = false + GROUP BY g.country + ORDER BY visitors DESC + LIMIT 5 + "#, + [week_start.into(), week_end.into()], + ); + + let rows = self.db.query_all(stmt).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + let visitors: i64 = row.try_get("", "visitors").ok()?; + let percentage = if total_visitors > 0 { + (visitors as f64 / total_visitors as f64) * 100.0 + } else { + 0.0 + }; + Some(GeographicData { + country: row.try_get("", "country").ok()?, + visitors, + percentage, + }) + }) + .collect()) + } + + /// Daily unique-session counts across the digest window, for the trend + /// sparkline. + async fn query_visitor_trend( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + date_trunc('day', timestamp) AS day, + COUNT(DISTINCT session_id)::bigint AS visitors + FROM events + WHERE timestamp BETWEEN $1 AND $2 + AND session_id IS NOT NULL + AND is_crawler = false + GROUP BY day + ORDER BY day ASC + "#, + [week_start.into(), week_end.into()], + ); + + let rows = self.db.query_all(stmt).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + Some(TrendPoint { + date: row.try_get("", "day").ok()?, + value: row.try_get("", "visitors").ok()?, + }) + }) + .collect()) + } + /// Aggregate deployment and infrastructure data async fn aggregate_deployment_data( &self, @@ -216,39 +431,329 @@ impl DigestService { }) } - /// Aggregate error and reliability data + /// Aggregate error and reliability data from `error_events`, + /// `error_groups`, and `external_service_health_checks`. async fn aggregate_error_data( &self, - _week_start: DateTime, - _week_end: DateTime, + week_start: DateTime, + week_end: DateTime, ) -> Result { - // TODO: Implement error aggregation from temps-logs or temps-analytics - // For now, return basic data + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + // Total error events captured in the window. + let total_errors = error_events::Entity::find() + .filter(error_events::Column::Timestamp.between(week_start, week_end)) + .count(self.db.as_ref()) + .await? as i64; + + // Error groups first seen this week — "new error types". + let new_error_types = error_groups::Entity::find() + .filter(error_groups::Column::FirstSeen.between(week_start, week_end)) + .count(self.db.as_ref()) + .await? as i64; + + // Distinct visitors affected by errors this week. + let affected_users = error_events::Entity::find() + .filter(error_events::Column::Timestamp.between(week_start, week_end)) + .filter(error_events::Column::VisitorId.is_not_null()) + .select_only() + .column(error_events::Column::VisitorId) + .distinct() + .count(self.db.as_ref()) + .await? as i64; + + // Most common errors this week, grouped by error group. + let most_common_errors = self + .query_most_common_errors(week_start, week_end) + .await + .unwrap_or_default(); + + // Daily error counts for the trend sparkline. + let error_trend = self + .query_error_trend(week_start, week_end) + .await + .unwrap_or_default(); + + // Health-check based uptime. `external_service_health_checks.status` + // is "operational" | "degraded" | "down". Uptime = share of checks + // that were operational; failed = degraded + down. + let (uptime_percentage, failed_health_checks) = { + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + COUNT(*) FILTER (WHERE status = 'operational')::float8 AS operational, + COUNT(*) FILTER (WHERE status <> 'operational')::bigint AS failed, + COUNT(*)::float8 AS total + FROM external_service_health_checks + WHERE checked_at BETWEEN $1 AND $2 + "#, + [week_start.into(), week_end.into()], + ); + match self.db.query_one(stmt).await? { + Some(row) => { + let operational: f64 = row.try_get("", "operational").unwrap_or(0.0); + let failed: i64 = row.try_get("", "failed").unwrap_or(0); + let total: f64 = row.try_get("", "total").unwrap_or(0.0); + if total > 0.0 { + ((operational / total) * 100.0, failed) + } else { + // No health checks recorded — report 100% rather than + // a fabricated 99.9, and zero failures. + (100.0, 0) + } + } + None => (100.0, 0), + } + }; + + // Error rate: errors per 1,000 page views this week. Gives the number + // meaning relative to traffic instead of a raw count. + let page_views = events::Entity::find() + .filter(events::Column::Timestamp.between(week_start, week_end)) + .count(self.db.as_ref()) + .await? as i64; + let error_rate = if page_views > 0 { + (total_errors as f64 / page_views as f64) * 1000.0 + } else { + 0.0 + }; + Ok(ErrorData { - total_errors: 0, - error_rate: 0.0, - new_error_types: 0, - most_common_errors: vec![], - affected_users: 0, - error_trend: vec![], - uptime_percentage: 99.9, - failed_health_checks: 0, + total_errors, + error_rate, + new_error_types, + most_common_errors, + affected_users, + error_trend, + uptime_percentage, + failed_health_checks, }) } - /// Aggregate funnel and conversion data + /// Top error groups by event count this week (max 5). + async fn query_most_common_errors( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + g.title AS error_type, + COUNT(e.id)::bigint AS count, + MIN(e.timestamp) AS first_occurrence, + MAX(e.timestamp) AS last_occurrence, + COUNT(DISTINCT e.visitor_id)::bigint AS affected_sessions + FROM error_events e + JOIN error_groups g ON g.id = e.error_group_id + WHERE e.timestamp BETWEEN $1 AND $2 + GROUP BY g.id, g.title + ORDER BY count DESC + LIMIT 5 + "#, + [week_start.into(), week_end.into()], + ); + + let rows = self.db.query_all(stmt).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + Some(CommonError { + error_type: row.try_get("", "error_type").ok()?, + count: row.try_get("", "count").ok()?, + first_occurrence: row.try_get("", "first_occurrence").ok()?, + last_occurrence: row.try_get("", "last_occurrence").ok()?, + affected_sessions: row.try_get("", "affected_sessions").ok()?, + }) + }) + .collect()) + } + + /// Daily error-event counts across the digest window. + async fn query_error_trend( + &self, + week_start: DateTime, + week_end: DateTime, + ) -> Result> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + date_trunc('day', timestamp) AS day, + COUNT(*)::bigint AS errors + FROM error_events + WHERE timestamp BETWEEN $1 AND $2 + GROUP BY day + ORDER BY day ASC + "#, + [week_start.into(), week_end.into()], + ); + + let rows = self.db.query_all(stmt).await?; + Ok(rows + .into_iter() + .filter_map(|row| { + Some(TrendPoint { + date: row.try_get("", "day").ok()?, + value: row.try_get("", "errors").ok()?, + }) + }) + .collect()) + } + + /// Aggregate funnel and conversion data from `funnels` / `funnel_steps`. + /// + /// For each active funnel, a session "enters" if it fired the first step's + /// event and "completes" if it also fired the last step's event within the + /// window. Conversion is completions / entries, compared against the prior + /// week for the trend. async fn aggregate_funnel_data( &self, - __week_start: DateTime, - __week_end: DateTime, + week_start: DateTime, + week_end: DateTime, ) -> Result { - // TODO: Implement funnel aggregation from temps-analytics-funnels + let funnels = funnels::Entity::find() + .filter(funnels::Column::IsActive.eq(true)) + .all(self.db.as_ref()) + .await?; + + let total_funnels = funnels.len() as i64; + let prev_week_start = week_start - Duration::days(7); + + let mut funnel_stats = Vec::new(); + for funnel in funnels { + let steps = funnel_steps::Entity::find() + .filter(funnel_steps::Column::FunnelId.eq(funnel.id)) + .order_by_asc(funnel_steps::Column::StepOrder) + .all(self.db.as_ref()) + .await?; + + // A funnel needs at least one step to be measurable. + let Some(first_step) = steps.first() else { + continue; + }; + let last_step = steps.last().unwrap_or(first_step); + + let (entries, completions) = self + .funnel_entries_completions( + funnel.id, + &first_step.event_name, + &last_step.event_name, + week_start, + week_end, + ) + .await + .unwrap_or((0, 0)); + + let (prev_entries, prev_completions) = self + .funnel_entries_completions( + funnel.id, + &first_step.event_name, + &last_step.event_name, + prev_week_start, + week_start, + ) + .await + .unwrap_or((0, 0)); + + let completion_rate = if entries > 0 { + (completions as f64 / entries as f64) * 100.0 + } else { + 0.0 + }; + let prev_rate = if prev_entries > 0 { + (prev_completions as f64 / prev_entries as f64) * 100.0 + } else { + 0.0 + }; + let week_over_week_change = completion_rate - prev_rate; + + funnel_stats.push(FunnelStat { + funnel_name: funnel.name, + completion_rate, + drop_off_rate: 100.0 - completion_rate, + week_over_week_change, + total_entries: entries, + total_completions: completions, + }); + } + + // Most-trafficked funnels first. + funnel_stats.sort_by_key(|f| std::cmp::Reverse(f.total_entries)); + Ok(FunnelData { - total_funnels: 0, - funnel_stats: vec![], + total_funnels, + funnel_stats, }) } + /// Count sessions that entered (fired `first_event`) and completed (also + /// fired `last_event`) a funnel within `[start, end)`. + async fn funnel_entries_completions( + &self, + funnel_id: i32, + first_event: &str, + last_event: &str, + start: DateTime, + end: DateTime, + ) -> Result<(i64, i64)> { + use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; + + // Single funnel-step funnels: entry == completion. + let same_step = first_event == last_event; + + let stmt = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + WITH entered AS ( + SELECT DISTINCT session_id + FROM events + WHERE timestamp >= $1 AND timestamp < $2 + AND session_id IS NOT NULL + AND is_crawler = false + AND COALESCE(event_name, event_type) = $3 + ), + completed AS ( + SELECT DISTINCT session_id + FROM events + WHERE timestamp >= $1 AND timestamp < $2 + AND session_id IS NOT NULL + AND is_crawler = false + AND COALESCE(event_name, event_type) = $4 + ) + SELECT + (SELECT COUNT(*) FROM entered)::bigint AS entries, + (SELECT COUNT(*) FROM entered e + WHERE $5 OR e.session_id IN (SELECT session_id FROM completed))::bigint + AS completions + "#, + [ + start.into(), + end.into(), + first_event.into(), + last_event.into(), + same_step.into(), + ], + ); + + // `funnel_id` is accepted for future per-funnel event filtering; the + // current model identifies funnel membership purely by event name. + let _ = funnel_id; + + if let Some(row) = self.db.query_one(stmt).await? { + let entries: i64 = row.try_get("", "entries").unwrap_or(0); + let completions: i64 = row.try_get("", "completions").unwrap_or(0); + return Ok((entries, completions)); + } + Ok((0, 0)) + } + /// Aggregate individual project statistics async fn aggregate_project_data( &self, @@ -996,4 +1501,414 @@ mod tests { test_db.cleanup_all_tables().await.expect("Cleanup failed"); } + + // ── Error aggregation ─────────────────────────────────────────────── + + #[tokio::test] + async fn test_aggregate_error_data_empty() { + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + let errors = service + .aggregate_error_data(week_start, now) + .await + .expect("Failed to aggregate error data"); + + // With no error events and no health checks, the digest must report + // zeros and 100% uptime — never the old fabricated 99.9%. + assert_eq!(errors.total_errors, 0); + assert_eq!(errors.new_error_types, 0); + assert_eq!(errors.affected_users, 0); + assert_eq!(errors.failed_health_checks, 0); + assert_eq!(errors.uptime_percentage, 100.0); + assert!(errors.most_common_errors.is_empty()); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } + + #[tokio::test] + async fn test_aggregate_error_data_with_real_errors() { + use temps_entities::{error_events, error_groups, visitor}; + + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + let project = projects::ActiveModel { + name: Set("err-project".to_string()), + slug: Set("err-project".to_string()), + repo_name: Set("err-repo".to_string()), + repo_owner: Set("err-owner".to_string()), + directory: Set("/".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::Astro), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let project = project.insert(test_db.connection()).await.unwrap(); + + let environment = environments::ActiveModel { + project_id: Set(project.id), + name: Set("production".to_string()), + slug: Set("production".to_string()), + subdomain: Set("production".to_string()), + host: Set("production.example.com".to_string()), + upstreams: Set(temps_entities::upstream_config::UpstreamList::default()), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let environment = environment.insert(test_db.connection()).await.unwrap(); + + // error_events.visitor_id has an FK to `visitor` — create two. + let mut visitor_ids = Vec::new(); + for i in 0..2 { + let v = visitor::ActiveModel { + visitor_id: Set(format!("visitor-{}", i)), + project_id: Set(project.id), + environment_id: Set(environment.id), + first_seen: Set(now - Duration::days(1)), + last_seen: Set(now), + is_crawler: Set(false), + has_activity: Set(true), + ..Default::default() + }; + visitor_ids.push(v.insert(test_db.connection()).await.unwrap().id); + } + + // An error group first seen this week → counts as a new error type. + let group = error_groups::ActiveModel { + title: Set("TypeError: undefined is not a function".to_string()), + error_type: Set("TypeError".to_string()), + first_seen: Set(now - Duration::days(2)), + last_seen: Set(now), + total_count: Set(3), + status: Set("unresolved".to_string()), + project_id: Set(project.id), + created_at: Set(now - Duration::days(2)), + updated_at: Set(now), + ..Default::default() + }; + let group = group.insert(test_db.connection()).await.unwrap(); + + // Three error events this week, two distinct visitors. + for i in 0..3 { + let event = error_events::ActiveModel { + error_group_id: Set(group.id), + project_id: Set(project.id), + fingerprint_hash: Set(format!("fp-{}", i)), + timestamp: Set(now - Duration::hours((i + 1) as i64)), + exception_type: Set("TypeError".to_string()), + exception_value: Set(Some("undefined is not a function".to_string())), + source: Set(Some("custom".to_string())), + visitor_id: Set(Some(if i < 2 { + visitor_ids[0] + } else { + visitor_ids[1] + })), + created_at: Set(now - Duration::hours((i + 1) as i64)), + ..Default::default() + }; + event.insert(test_db.connection()).await.unwrap(); + } + + let errors = service + .aggregate_error_data(week_start, now) + .await + .expect("Failed to aggregate error data"); + + assert_eq!(errors.total_errors, 3); + assert_eq!(errors.new_error_types, 1); + assert_eq!(errors.affected_users, 2); + assert_eq!(errors.most_common_errors.len(), 1); + assert_eq!(errors.most_common_errors[0].count, 3); + // No health checks recorded → 100% uptime, not a fabricated value. + assert_eq!(errors.uptime_percentage, 100.0); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } + + #[tokio::test] + async fn test_aggregate_error_data_uptime_from_health_checks() { + use temps_entities::{external_service_health_checks as hc, external_services}; + + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + // Health checks have an FK to external_services — create one first. + let svc = external_services::ActiveModel { + name: Set("test-postgres".to_string()), + service_type: Set("postgres".to_string()), + status: Set("running".to_string()), + topology: Set("standalone".to_string()), + consecutive_health_failures: Set(0), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let svc = svc.insert(test_db.connection()).await.unwrap(); + + // 8 operational + 2 down = 80% uptime, 2 failed checks. + for i in 0..10 { + let status = if i < 8 { "operational" } else { "down" }; + let check = hc::ActiveModel { + service_id: Set(svc.id), + checked_at: Set(now - Duration::hours((i + 1) as i64)), + status: Set(status.to_string()), + response_time_ms: Set(Some(100)), + error_message: Set(None), + ..Default::default() + }; + check.insert(test_db.connection()).await.unwrap(); + } + + let errors = service + .aggregate_error_data(week_start, now) + .await + .expect("Failed to aggregate error data"); + + assert!((errors.uptime_percentage - 80.0).abs() < 0.01); + assert_eq!(errors.failed_health_checks, 2); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } + + // ── Funnel aggregation ────────────────────────────────────────────── + + #[tokio::test] + async fn test_aggregate_funnel_data_empty() { + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + let funnels = service + .aggregate_funnel_data(week_start, now) + .await + .expect("Failed to aggregate funnel data"); + + assert_eq!(funnels.total_funnels, 0); + assert!(funnels.funnel_stats.is_empty()); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } + + #[tokio::test] + async fn test_aggregate_funnel_data_with_conversions() { + use temps_entities::{funnel_steps, funnels}; + + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + let project = projects::ActiveModel { + name: Set("funnel-project".to_string()), + slug: Set("funnel-project".to_string()), + repo_name: Set("funnel-repo".to_string()), + repo_owner: Set("funnel-owner".to_string()), + directory: Set("/".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::Astro), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let project = project.insert(test_db.connection()).await.unwrap(); + + let environment = environments::ActiveModel { + project_id: Set(project.id), + name: Set("production".to_string()), + slug: Set("production".to_string()), + subdomain: Set("production".to_string()), + host: Set("production.example.com".to_string()), + upstreams: Set(temps_entities::upstream_config::UpstreamList::default()), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let environment = environment.insert(test_db.connection()).await.unwrap(); + + let deployment = deployments::ActiveModel { + project_id: Set(project.id), + environment_id: Set(environment.id), + slug: Set("deploy-funnel".to_string()), + state: Set("completed".to_string()), + metadata: Set(Some(Default::default())), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let deployment = deployment.insert(test_db.connection()).await.unwrap(); + + // Funnel: signup_started → signup_completed. + let funnel = funnels::ActiveModel { + project_id: Set(project.id), + name: Set("Signup".to_string()), + description: Set(None), + is_active: Set(true), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let funnel = funnel.insert(test_db.connection()).await.unwrap(); + + for (order, event_name) in [(1, "signup_started"), (2, "signup_completed")] { + let step = funnel_steps::ActiveModel { + funnel_id: Set(funnel.id), + step_order: Set(order), + event_name: Set(event_name.to_string()), + event_filter: Set(None), + created_at: Set(now), + ..Default::default() + }; + step.insert(test_db.connection()).await.unwrap(); + } + + // Helper to insert a custom event for a session. + let insert_event = + |session: &str, event_name: &str, ts: DateTime| events::ActiveModel { + timestamp: Set(ts), + project_id: Set(project.id), + environment_id: Set(Some(environment.id)), + deployment_id: Set(Some(deployment.id)), + session_id: Set(Some(session.to_string())), + hostname: Set("example.com".to_string()), + pathname: Set("/".to_string()), + page_path: Set("/".to_string()), + href: Set("https://example.com/".to_string()), + event_type: Set("custom".to_string()), + event_name: Set(Some(event_name.to_string())), + is_crawler: Set(false), + ..Default::default() + }; + + // 4 sessions enter, 3 of them complete this week. Events are placed + // strictly inside the window — funnel aggregation uses `< week_end`. + for i in 0..4 { + let sid = format!("s{}", i); + let ts = now - Duration::hours((i + 1) as i64); + insert_event(&sid, "signup_started", ts) + .insert(test_db.connection()) + .await + .unwrap(); + if i < 3 { + insert_event(&sid, "signup_completed", ts) + .insert(test_db.connection()) + .await + .unwrap(); + } + } + + let funnel_data = service + .aggregate_funnel_data(week_start, now) + .await + .expect("Failed to aggregate funnel data"); + + assert_eq!(funnel_data.total_funnels, 1); + assert_eq!(funnel_data.funnel_stats.len(), 1); + let stat = &funnel_data.funnel_stats[0]; + assert_eq!(stat.funnel_name, "Signup"); + assert_eq!(stat.total_entries, 4); + assert_eq!(stat.total_completions, 3); + assert!((stat.completion_rate - 75.0).abs() < 0.01); + assert!((stat.drop_off_rate - 25.0).abs() < 0.01); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } + + // ── Performance detail aggregation ────────────────────────────────── + + #[tokio::test] + async fn test_aggregate_performance_top_pages_and_bounce() { + let (service, test_db) = setup_test_service().await; + + let now = Utc::now(); + let week_start = now - Duration::days(7); + + let project = projects::ActiveModel { + name: Set("perf-project".to_string()), + slug: Set("perf-project".to_string()), + repo_name: Set("perf-repo".to_string()), + repo_owner: Set("perf-owner".to_string()), + directory: Set("/".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::Astro), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let project = project.insert(test_db.connection()).await.unwrap(); + + let environment = environments::ActiveModel { + project_id: Set(project.id), + name: Set("production".to_string()), + slug: Set("production".to_string()), + subdomain: Set("production".to_string()), + host: Set("production.example.com".to_string()), + upstreams: Set(temps_entities::upstream_config::UpstreamList::default()), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let environment = environment.insert(test_db.connection()).await.unwrap(); + + let deployment = deployments::ActiveModel { + project_id: Set(project.id), + environment_id: Set(environment.id), + slug: Set("deploy-perf".to_string()), + state: Set("completed".to_string()), + metadata: Set(Some(Default::default())), + created_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + let deployment = deployment.insert(test_db.connection()).await.unwrap(); + + // 3 sessions: 2 land on /pricing (one bounces), 1 on /docs. + let pages = [ + ("sess-a", "/pricing", true), + ("sess-b", "/pricing", false), + ("sess-c", "/docs", false), + ]; + for (sid, path, bounce) in pages { + let event = events::ActiveModel { + timestamp: Set(now - Duration::hours(1)), + project_id: Set(project.id), + environment_id: Set(Some(environment.id)), + deployment_id: Set(Some(deployment.id)), + session_id: Set(Some(sid.to_string())), + hostname: Set("example.com".to_string()), + pathname: Set(path.to_string()), + page_path: Set(path.to_string()), + href: Set(format!("https://example.com{}", path)), + is_entry: Set(true), + is_bounce: Set(bounce), + event_type: Set("pageview".to_string()), + is_crawler: Set(false), + ..Default::default() + }; + event.insert(test_db.connection()).await.unwrap(); + } + + let perf = service + .aggregate_performance_data(week_start, now) + .await + .expect("Failed to aggregate performance data"); + + // Top pages: /pricing has 2 views, /docs has 1. + assert_eq!(perf.top_pages.len(), 2); + assert_eq!(perf.top_pages[0].path, "/pricing"); + assert_eq!(perf.top_pages[0].views, 2); + // Bounce rate: 1 of 3 sessions bounced. + assert!((perf.bounce_rate - 33.33).abs() < 0.1); + + test_db.cleanup_all_tables().await.expect("Cleanup failed"); + } } diff --git a/crates/temps-notifications/src/digest/templates.rs b/crates/temps-notifications/src/digest/templates.rs index 7143525f..b8331b20 100644 --- a/crates/temps-notifications/src/digest/templates.rs +++ b/crates/temps-notifications/src/digest/templates.rs @@ -1,494 +1,793 @@ -//! Email templates for weekly digest +//! Email templates for weekly digest. +//! +//! HTML email rendering rules followed here (do not "modernize" away): +//! - Layout is **table-based**, never `display: flex` / `display: grid`. +//! Gmail, Outlook, and most mobile clients strip or ignore modern CSS +//! layout, which collapses flex/grid children into one another (the +//! `Visitors: 18Page Views: 26` regression). Tables render identically +//! everywhere. +//! - Critical styling (spacing, colors, widths) is **inlined** on each +//! element. Many clients drop the ` - - -
-
-

📊 Weekly Digest - {}

-
- Week of {} to {} -
-
-"#, - project_name, - digest.week_start.format("%b %d, %Y"), - digest.week_end.format("%b %d, %Y") - ); - - // Executive Summary - html.push_str(&format!( - r#" -
-
📈 Executive Summary
-
-
-
Total Visitors
-
{}
-
{:+.1}%
-
-
-
Deployments
-
{}
-
({} failed)
-
-
-
New Errors
-
{}
-
-
-
Uptime
-
{:.1}%
-
-
-
-"#, - format_number(digest.executive_summary.total_visitors), - if digest.executive_summary.visitor_change_percent >= 0.0 { - "positive" - } else { - "negative" - }, - digest.executive_summary.visitor_change_percent, - digest.executive_summary.total_deployments, - if digest.executive_summary.failed_deployments == 0 { - "neutral" - } else { - "negative" - }, - digest.executive_summary.failed_deployments, - digest.executive_summary.new_errors, - digest.executive_summary.uptime_percent - )); - - // Performance Section - if let Some(perf) = &digest.performance { - html.push_str(&format!( - r#" -
-
👥 Performance & Analytics
-
-
-
Total Visitors
-
{}
-
-
-
Page Views
-
{}
-
-
-
Unique Sessions
-
{}
-
-
-
Week/Week Change
-
{:+.1}%
-
-
-
-"#, - format_number(perf.total_visitors), - format_number(perf.page_views), - format_number(perf.unique_sessions), - if perf.week_over_week_change >= 0.0 { - "positive" + let mut body = String::new(); + + // ── Executive summary ─────────────────────────────────────────────── + let summary = &digest.executive_summary; + body.push_str(§ion_open("📈 Executive Summary")); + body.push_str(&metric_grid(&[ + Metric::new("Total Visitors", &format_number(summary.total_visitors)) + .with_trend(summary.visitor_change_percent), + Metric::new("Deployments", &summary.total_deployments.to_string()).with_note( + &format!("{} failed", summary.failed_deployments), + if summary.failed_deployments == 0 { + Tone::Neutral } else { - "negative" + Tone::Negative }, - perf.week_over_week_change - )); + ), + Metric::new("New Errors", &format_number(summary.new_errors)).with_note( + "this week", + if summary.new_errors == 0 { + Tone::Neutral + } else { + Tone::Negative + }, + ), + Metric::new("Uptime", &format!("{:.1}%", summary.uptime_percent)) + .with_note("of the week", uptime_tone(summary.uptime_percent)), + ])); + body.push_str(§ion_close()); + + // ── Performance ───────────────────────────────────────────────────── + if let Some(perf) = &digest.performance { + body.push_str(§ion_open("👥 Performance & Analytics")); + body.push_str(&metric_grid(&[ + Metric::new("Total Visitors", &format_number(perf.total_visitors)), + Metric::new("Page Views", &format_number(perf.page_views)), + Metric::new( + "Avg. Session", + &format_duration(perf.average_session_duration), + ), + Metric::new("Bounce Rate", &format!("{:.1}%", perf.bounce_rate)), + ])); + + if !perf.top_pages.is_empty() { + body.push_str(&subhead("Top Pages")); + let rows: Vec<[String; 3]> = perf + .top_pages + .iter() + .take(5) + .map(|p| { + [ + escape_html(&p.path), + format_number(p.views), + format_number(p.unique_visitors), + ] + }) + .collect(); + body.push_str(&data_table( + &["Page", "Views", "Visitors"], + &rows, + &[Align::Left, Align::Right, Align::Right], + )); + } + + if !perf.geographic_distribution.is_empty() { + body.push_str(&subhead("Top Countries")); + let rows: Vec<[String; 3]> = perf + .geographic_distribution + .iter() + .take(5) + .map(|g| { + [ + escape_html(&g.country), + format_number(g.visitors), + format!("{:.1}%", g.percentage), + ] + }) + .collect(); + body.push_str(&data_table( + &["Country", "Visitors", "Share"], + &rows, + &[Align::Left, Align::Right, Align::Right], + )); + } + body.push_str(§ion_close()); } - // Deployments Section + // ── Deployments ───────────────────────────────────────────────────── if let Some(deploy) = &digest.deployments { - html.push_str(&format!( - r#" -
-
🚀 Deployments & Infrastructure
-
-
-
Total Deployments
-
{}
-
-
-
Success Rate
-
{:.1}%
-
-
-
Successful
-
{}
-
-
-
Failed
-
{}
-
-
-
-"#, - deploy.total_deployments, - deploy.success_rate, - deploy.successful_deployments, - deploy.failed_deployments - )); + body.push_str(§ion_open("🚀 Deployments & Infrastructure")); + body.push_str(&metric_grid(&[ + Metric::new("Total", &deploy.total_deployments.to_string()), + Metric::new("Success Rate", &format!("{:.1}%", deploy.success_rate)) + .with_note("", success_rate_tone(deploy.success_rate)), + Metric::new("Successful", &deploy.successful_deployments.to_string()), + Metric::new("Failed", &deploy.failed_deployments.to_string()).with_note( + "", + if deploy.failed_deployments == 0 { + Tone::Neutral + } else { + Tone::Negative + }, + ), + ])); + body.push_str(§ion_close()); } - // Errors Section + // ── Errors & reliability ──────────────────────────────────────────── if let Some(errors) = &digest.errors { - html.push_str(&format!( - r#" -
-
⚠️ Errors & Reliability
-
-
-
Total Errors
-
{}
-
-
-
New Error Types
-
{}
-
-
-
Uptime
-
{:.2}%
-
-
-
Failed Health Checks
-
{}
-
-
-
-"#, - format_number(errors.total_errors), - errors.new_error_types, - errors.uptime_percentage, - errors.failed_health_checks - )); + body.push_str(§ion_open("⚠️ Errors & Reliability")); + body.push_str(&metric_grid(&[ + Metric::new("Total Errors", &format_number(errors.total_errors)).with_note( + "", + if errors.total_errors == 0 { + Tone::Positive + } else { + Tone::Negative + }, + ), + Metric::new("New Error Types", &errors.new_error_types.to_string()), + Metric::new("Uptime", &format!("{:.2}%", errors.uptime_percentage)) + .with_note("", uptime_tone(errors.uptime_percentage)), + Metric::new( + "Failed Health Checks", + &errors.failed_health_checks.to_string(), + ) + .with_note( + "", + if errors.failed_health_checks == 0 { + Tone::Positive + } else { + Tone::Negative + }, + ), + ])); + + if !errors.most_common_errors.is_empty() { + body.push_str(&subhead("Most Common Errors")); + let rows: Vec<[String; 3]> = errors + .most_common_errors + .iter() + .take(5) + .map(|e| { + [ + escape_html(&e.error_type), + format_number(e.count), + format_number(e.affected_sessions), + ] + }) + .collect(); + body.push_str(&data_table( + &["Error", "Occurrences", "Sessions"], + &rows, + &[Align::Left, Align::Right, Align::Right], + )); + } + body.push_str(§ion_close()); } - // Projects Section - if !digest.projects.is_empty() { - html.push_str( - r#" -
-
📦 Project Activity
-"#, - ); - - for project in &digest.projects { - let trend_class = if project.week_over_week_change >= 0.0 { - "positive" + // ── Funnels ───────────────────────────────────────────────────────── + if let Some(funnels) = &digest.funnels { + if funnels.total_funnels > 0 { + body.push_str(§ion_open("🎯 Conversion Funnels")); + if funnels.funnel_stats.is_empty() { + body.push_str(&empty_note(&format!( + "{} funnel(s) configured — no entries recorded this week.", + funnels.total_funnels + ))); } else { - "negative" - }; - - html.push_str(&format!( - r#" -
-
{}
-
- Visitors: {} - Page Views: {} - Sessions: {} - Deployments: {} - Trend: {:+.1}% -
-
-"#, - project.project_name, - format_number(project.visitors), - format_number(project.page_views), - format_number(project.unique_sessions), - project.deployments, - trend_class, - project.week_over_week_change - )); + for stat in &funnels.funnel_stats { + body.push_str(&funnel_card(stat)); + } + } + body.push_str(§ion_close()); } + } - html.push_str("
\n"); + // ── Project activity ──────────────────────────────────────────────── + if !digest.projects.is_empty() { + body.push_str(§ion_open("📦 Project Activity")); + for project in &digest.projects { + body.push_str(&project_card(project)); + } + body.push_str(§ion_close()); } - // Footer - html.push_str( - r#" - + Ok(wrap_document( + project_name, + digest.week_start.format("%b %d, %Y").to_string(), + digest.week_end.format("%b %d, %Y").to_string(), + &body, + )) +} + +// ── Document shell ────────────────────────────────────────────────────────── + +fn wrap_document(project_name: &str, week_start: String, week_end: String, body: &str) -> String { + format!( + r#" + + + + + +Weekly Digest + + + + +
+ + + + +
+
📊 Weekly Digest
+
{project_name}  ·  {week_start} – {week_end}
+
{body}
+
+ This is an automated weekly digest from Temps.
+ Manage your notification preferences in your account settings.
+
+
"#, + page_bg = PAGE_BG, + border = BORDER, + brand = BRAND, + muted = MUTED, + project_name = escape_html(project_name), + week_start = week_start, + week_end = week_end, + body = body, + ) +} + +// ── Section helpers ───────────────────────────────────────────────────────── + +fn section_open(title: &str) -> String { + format!( + r#" + +
{title}
"#, + ink = INK, + border = BORDER, + title = escape_html(title), + ) +} + +fn section_close() -> String { + "
".to_string() +} + +fn subhead(text: &str) -> String { + format!( + r#"
{text}
"#, + muted = MUTED, + text = escape_html(text), + ) +} + +fn empty_note(text: &str) -> String { + format!( + r#"
{text}
"#, + muted = MUTED, + card = CARD_BG, + text = escape_html(text), + ) +} + +// ── Metric cards (2-column table, never flex/grid) ────────────────────────── + +#[derive(Clone, Copy)] +enum Tone { + Positive, + Negative, + Neutral, +} + +impl Tone { + fn fg(self) -> &'static str { + match self { + Tone::Positive => POSITIVE, + Tone::Negative => NEGATIVE, + Tone::Neutral => NEUTRAL, + } + } + fn bg(self) -> &'static str { + match self { + Tone::Positive => POSITIVE_BG, + Tone::Negative => NEGATIVE_BG, + Tone::Neutral => NEUTRAL_BG, + } + } +} + +struct Metric { + label: String, + value: String, + note: Option<(String, Tone)>, +} + +impl Metric { + fn new(label: &str, value: &str) -> Self { + Self { + label: label.to_string(), + value: value.to_string(), + note: None, + } + } + fn with_note(mut self, note: &str, tone: Tone) -> Self { + if !note.is_empty() { + self.note = Some((note.to_string(), tone)); + } + self + } + fn with_trend(mut self, change: f64) -> Self { + let tone = if change > 0.0 { + Tone::Positive + } else if change < 0.0 { + Tone::Negative + } else { + Tone::Neutral + }; + self.note = Some((format!("{:+.1}% vs last week", change), tone)); + self + } +} + +/// Render metrics as a 2-per-row table. Each cell is a fixed 50% width so the +/// layout is stable in every client. +fn metric_grid(metrics: &[Metric]) -> String { + let mut out = String::from( + r#""#, ); + for pair in metrics.chunks(2) { + out.push_str(""); + for i in 0..2 { + // 8px gutter via cell padding; empty cell keeps the grid aligned + // when there is an odd number of metrics. + let pad = if i == 0 { + "padding:6px 4px 6px 0;" + } else { + "padding:6px 0 6px 4px;" + }; + match pair.get(i) { + Some(m) => { + out.push_str(&format!( + r#""#, + pad = pad, + card = metric_card(m), + )); + } + None => out.push_str(r#""#), + } + } + out.push_str(""); + } + out.push_str("
{card}
"); + out +} - Ok(html) +fn metric_card(m: &Metric) -> String { + let note_html = match &m.note { + Some((text, tone)) => format!( + r#"
{text}
"#, + fg = tone.fg(), + bg = tone.bg(), + text = escape_html(text), + ), + None => String::new(), + }; + format!( + r#" +
+
{label}
+
{value}
+{note} +
"#, + card = CARD_BG, + brand = BRAND, + muted = MUTED, + ink = INK, + label = escape_html(&m.label), + value = escape_html(&m.value), + note = note_html, + ) } -/// Render plain text email template for weekly digest -pub fn render_text_template(digest: &WeeklyDigestData) -> Result { - let project_name = digest.project_name.as_deref().unwrap_or("Your Project"); +// ── Data tables (top pages / countries / errors) ──────────────────────────── - let mut text = format!( - r#"📊 WEEKLY DIGEST - {} -Week of {} to {} +#[derive(Clone, Copy)] +enum Align { + Left, + Right, +} -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📈 EXECUTIVE SUMMARY -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +impl Align { + fn as_str(self) -> &'static str { + match self { + Align::Left => "left", + Align::Right => "right", + } + } +} -• {} total visitors ({:+.1}% from last week) -• {} deployments ({} failed) -• {} new errors detected -• {:.1}% uptime +fn data_table( + headers: &[&str; N], + rows: &[[String; N]], + aligns: &[Align; N], +) -> String { + let mut out = String::from( + r#""#, + ); + // Header row. + out.push_str(""); + for i in 0..N { + out.push_str(&format!( + r#""#, + align = aligns[i].as_str(), + muted = MUTED, + border = BORDER, + h = escape_html(headers[i]), + )); + } + out.push_str(""); + // Body rows. + for row in rows { + out.push_str(""); + for i in 0..N { + out.push_str(&format!( + r#""#, + align = aligns[i].as_str(), + ink = INK, + border = BORDER, + v = row[i], + )); + } + out.push_str(""); + } + out.push_str("
{h}
{v}
"); + out +} -"#, +// ── Funnel + project cards ────────────────────────────────────────────────── + +fn funnel_card(stat: &FunnelStat) -> String { + let trend_tone = if stat.week_over_week_change > 0.0 { + Tone::Positive + } else if stat.week_over_week_change < 0.0 { + Tone::Negative + } else { + Tone::Neutral + }; + format!( + r#" +
+
{name}
+ + + {entries} + {completions} + {rate} + +
+
{trend:+.1}% vs last week
+
"#, + card = CARD_BG, + brand = BRAND, + ink = INK, + name = escape_html(&stat.funnel_name), + entries = inline_stat("Entries", &format_number(stat.total_entries)), + completions = inline_stat("Completions", &format_number(stat.total_completions)), + rate = inline_stat("Conversion", &format!("{:.1}%", stat.completion_rate)), + trend_fg = trend_tone.fg(), + trend_bg = trend_tone.bg(), + trend = stat.week_over_week_change, + ) +} + +fn project_card(project: &ProjectStats) -> String { + let trend_tone = if project.week_over_week_change > 0.0 { + Tone::Positive + } else if project.week_over_week_change < 0.0 { + Tone::Negative + } else { + Tone::Neutral + }; + format!( + r#" +
+ + + + + +
{name}{trend:+.1}%
+ + + {visitors} + {page_views} + {sessions} + {deployments} + +
+
"#, + card = CARD_BG, + brand = BRAND, + ink = INK, + name = escape_html(&project.project_name), + trend_fg = trend_tone.fg(), + trend_bg = trend_tone.bg(), + trend = project.week_over_week_change, + visitors = inline_stat("Visitors", &format_number(project.visitors)), + page_views = inline_stat("Page Views", &format_number(project.page_views)), + sessions = inline_stat("Sessions", &format_number(project.unique_sessions)), + deployments = inline_stat("Deployments", &project.deployments.to_string()), + ) +} + +/// A single label-over-value stat as its own table cell. Putting each stat in +/// its own `` is what fixes the `Visitors: 18Page Views: 26` collision — +/// cells cannot run into each other the way inline ``s do. +fn inline_stat(label: &str, value: &str) -> String { + format!( + r#" +
{label}
+
{value}
+"#, + muted = MUTED, + ink = INK, + label = escape_html(label), + value = escape_html(value), + ) +} + +/// Render plain text email template for weekly digest. +pub fn render_text_template(digest: &WeeklyDigestData) -> Result { + let project_name = digest.project_name.as_deref().unwrap_or("Your Project"); + let rule = "═".repeat(52); + + let mut text = format!( + "📊 WEEKLY DIGEST - {}\nWeek of {} to {}\n\n{rule}\n📈 EXECUTIVE SUMMARY\n{rule}\n\n", project_name, digest.week_start.format("%b %d, %Y"), digest.week_end.format("%b %d, %Y"), - format_number(digest.executive_summary.total_visitors), - digest.executive_summary.visitor_change_percent, - digest.executive_summary.total_deployments, - digest.executive_summary.failed_deployments, - digest.executive_summary.new_errors, - digest.executive_summary.uptime_percent + rule = rule, ); - // Performance Section + let s = &digest.executive_summary; + text.push_str(&format!( + "• {} total visitors ({:+.1}% from last week)\n• {} deployments ({} failed)\n• {} new errors detected\n• {:.1}% uptime\n\n", + format_number(s.total_visitors), + s.visitor_change_percent, + s.total_deployments, + s.failed_deployments, + format_number(s.new_errors), + s.uptime_percent, + )); + if let Some(perf) = &digest.performance { text.push_str(&format!( - r#"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -👥 PERFORMANCE & ANALYTICS -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Total Visitors: {} -Page Views: {} -Unique Sessions: {} -Week/Week Change: {:+.1}% - -"#, + "{rule}\n👥 PERFORMANCE & ANALYTICS\n{rule}\n\nTotal Visitors: {}\nPage Views: {}\nUnique Sessions: {}\nAvg. Session: {}\nBounce Rate: {:.1}%\nWeek/Week Change: {:+.1}%\n\n", format_number(perf.total_visitors), format_number(perf.page_views), format_number(perf.unique_sessions), - perf.week_over_week_change + format_duration(perf.average_session_duration), + perf.bounce_rate, + perf.week_over_week_change, + rule = rule, )); + if !perf.top_pages.is_empty() { + text.push_str("Top Pages:\n"); + for p in perf.top_pages.iter().take(5) { + text.push_str(&format!( + " {} — {} views, {} visitors\n", + p.path, + format_number(p.views), + format_number(p.unique_visitors), + )); + } + text.push('\n'); + } + if !perf.geographic_distribution.is_empty() { + text.push_str("Top Countries:\n"); + for g in perf.geographic_distribution.iter().take(5) { + text.push_str(&format!( + " {} — {} visitors ({:.1}%)\n", + g.country, + format_number(g.visitors), + g.percentage, + )); + } + text.push('\n'); + } } - // Deployments Section if let Some(deploy) = &digest.deployments { text.push_str(&format!( - r#"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -🚀 DEPLOYMENTS & INFRASTRUCTURE -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Total Deployments: {} -Success Rate: {:.1}% -Successful: {} -Failed: {} - -"#, + "{rule}\n🚀 DEPLOYMENTS & INFRASTRUCTURE\n{rule}\n\nTotal Deployments: {}\nSuccess Rate: {:.1}%\nSuccessful: {}\nFailed: {}\n\n", deploy.total_deployments, deploy.success_rate, deploy.successful_deployments, - deploy.failed_deployments + deploy.failed_deployments, + rule = rule, )); } - // Errors Section if let Some(errors) = &digest.errors { text.push_str(&format!( - r#"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -⚠️ ERRORS & RELIABILITY -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -Total Errors: {} -New Error Types: {} -Uptime: {:.2}% -Failed Checks: {} - -"#, + "{rule}\n⚠️ ERRORS & RELIABILITY\n{rule}\n\nTotal Errors: {}\nNew Error Types: {}\nUptime: {:.2}%\nFailed Health Checks: {}\n\n", format_number(errors.total_errors), errors.new_error_types, errors.uptime_percentage, - errors.failed_health_checks + errors.failed_health_checks, + rule = rule, )); + if !errors.most_common_errors.is_empty() { + text.push_str("Most Common Errors:\n"); + for e in errors.most_common_errors.iter().take(5) { + text.push_str(&format!( + " {} — {} occurrences, {} sessions\n", + e.error_type, + format_number(e.count), + format_number(e.affected_sessions), + )); + } + text.push('\n'); + } } - // Projects Section - if !digest.projects.is_empty() { - text.push_str( - r#"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -📦 PROJECT ACTIVITY -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -"#, - ); + if let Some(funnels) = &digest.funnels { + if funnels.total_funnels > 0 { + text.push_str(&format!( + "{rule}\n🎯 CONVERSION FUNNELS\n{rule}\n\n", + rule = rule + )); + if funnels.funnel_stats.is_empty() { + text.push_str(&format!( + "{} funnel(s) configured — no entries recorded this week.\n\n", + funnels.total_funnels + )); + } else { + for stat in &funnels.funnel_stats { + text.push_str(&format!( + "{}:\n {} entries → {} completions | {:.1}% conversion | {:+.1}% vs last week\n\n", + stat.funnel_name, + format_number(stat.total_entries), + format_number(stat.total_completions), + stat.completion_rate, + stat.week_over_week_change, + )); + } + } + } + } + if !digest.projects.is_empty() { + text.push_str(&format!( + "{rule}\n📦 PROJECT ACTIVITY\n{rule}\n\n", + rule = rule + )); for project in &digest.projects { text.push_str(&format!( - r#"{name}: - Visitors: {visitors} | Page Views: {page_views} | Sessions: {sessions} | Deployments: {deployments} | Trend: {trend:+.1}% - -"#, + "{name}:\n Visitors: {visitors} | Page Views: {page_views} | Sessions: {sessions} | Deployments: {deployments} | Trend: {trend:+.1}%\n\n", name = project.project_name, visitors = format_number(project.visitors), page_views = format_number(project.page_views), sessions = format_number(project.unique_sessions), deployments = project.deployments, - trend = project.week_over_week_change + trend = project.week_over_week_change, )); } } - text.push_str( - r#"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -This is an automated weekly digest from Temps. -Manage your notification preferences in your account settings. -"#, - ); + text.push_str(&format!( + "{rule}\n\nThis is an automated weekly digest from Temps.\nManage your notification preferences in your account settings.\n", + rule = rule, + )); Ok(text) } -/// Format large numbers with commas -fn format_number(n: i64) -> String { - let s = n.to_string(); - let mut result = String::new(); +// ── Formatting helpers ────────────────────────────────────────────────────── - for (count, c) in s.chars().rev().enumerate() { +/// Format large numbers with thousands separators. +fn format_number(n: i64) -> String { + let negative = n < 0; + let digits = n.unsigned_abs().to_string(); + let mut grouped = String::new(); + for (count, c) in digits.chars().rev().enumerate() { if count > 0 && count % 3 == 0 { - result.push(','); + grouped.push(','); } - result.push(c); + grouped.push(c); } + let mut result: String = grouped.chars().rev().collect(); + if negative { + result.insert(0, '-'); + } + result +} - result.chars().rev().collect() +/// Format a duration given in minutes into a human-readable string. +fn format_duration(minutes: f64) -> String { + if minutes <= 0.0 { + return "0s".to_string(); + } + let total_seconds = (minutes * 60.0).round() as i64; + let mins = total_seconds / 60; + let secs = total_seconds % 60; + if mins == 0 { + format!("{}s", secs) + } else if secs == 0 { + format!("{}m", mins) + } else { + format!("{}m {}s", mins, secs) + } +} + +/// Escape a string for safe inclusion in HTML email content. Project names, +/// error types, and page paths are user-controlled. +fn escape_html(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + _ => out.push(c), + } + } + out +} + +fn uptime_tone(pct: f64) -> Tone { + if pct >= 99.9 { + Tone::Positive + } else if pct >= 99.0 { + Tone::Neutral + } else { + Tone::Negative + } +} + +fn success_rate_tone(pct: f64) -> Tone { + if pct >= 95.0 { + Tone::Positive + } else if pct >= 80.0 { + Tone::Neutral + } else { + Tone::Negative + } } #[cfg(test)] @@ -503,6 +802,25 @@ mod tests { assert_eq!(format_number(1234), "1,234"); assert_eq!(format_number(1234567), "1,234,567"); assert_eq!(format_number(1234567890), "1,234,567,890"); + assert_eq!(format_number(-4200), "-4,200"); + } + + #[test] + fn test_format_duration() { + assert_eq!(format_duration(0.0), "0s"); + assert_eq!(format_duration(-1.0), "0s"); + assert_eq!(format_duration(0.5), "30s"); + assert_eq!(format_duration(2.0), "2m"); + assert_eq!(format_duration(2.5), "2m 30s"); + } + + #[test] + fn test_escape_html() { + assert_eq!( + escape_html(""), + "<script>alert('x')</script>" + ); + assert_eq!(escape_html("a & b"), "a & b"); } #[test] @@ -516,6 +834,11 @@ mod tests { assert!(html.contains("")); assert!(html.contains("Weekly Digest")); assert!(html.contains("Executive Summary")); + // Email-safety: no flex/grid layout that collapses in Gmail/Outlook. + assert!(!html.contains("display:flex")); + assert!(!html.contains("display: flex")); + assert!(!html.contains("display:grid")); + assert!(!html.contains("display: grid")); } #[test] @@ -542,7 +865,11 @@ mod tests { page_views: 5678, average_session_duration: 5.5, bounce_rate: 30.0, - top_pages: vec![], + top_pages: vec![TopPage { + path: "/pricing".to_string(), + views: 900, + unique_visitors: 700, + }], geographic_distribution: vec![], visitor_trend: vec![], week_over_week_change: 15.0, @@ -550,10 +877,10 @@ mod tests { let html = render_html_template(&digest).expect("Failed to render HTML template"); - assert!(html.contains("1,234")); // Total visitors formatted - assert!(html.contains("5,678")); // Page views formatted - assert!(html.contains("Performance")); // Section exists - assert!(html.contains("Analytics")); // Section exists + assert!(html.contains("1,234")); + assert!(html.contains("5,678")); + assert!(html.contains("Performance")); + assert!(html.contains("/pricing")); // top page rendered } #[test] @@ -576,8 +903,35 @@ mod tests { let text = render_text_template(&digest).expect("Failed to render text template"); - assert!(text.contains("45")); // Total deployments - assert!(text.contains("93.3%")); // Success rate + assert!(text.contains("45")); + assert!(text.contains("93.3%")); assert!(text.contains("DEPLOYMENTS & INFRASTRUCTURE")); } + + #[test] + fn test_project_card_stats_do_not_collide() { + // Regression test for the `Visitors: 18Page Views: 26` bug: each stat + // must be in its own table cell, never inline spans. + let now = Utc::now(); + let week_start = now - chrono::Duration::days(7); + let mut digest = WeeklyDigestData::new(week_start, now); + digest.projects = vec![ProjectStats { + project_id: 1, + project_name: "davidviejo-dev".to_string(), + project_slug: "davidviejo-dev".to_string(), + visitors: 18, + page_views: 26, + unique_sessions: 18, + deployments: 0, + week_over_week_change: -14.3, + }]; + + let html = render_html_template(&digest).expect("render"); + // Labels and values are in separate cells, so the rendered output must + // never contain the run-together strings. + assert!(!html.contains("18Page")); + assert!(!html.contains("26Sessions")); + assert!(html.contains("davidviejo-dev")); + assert!(html.contains("Project Activity")); + } } diff --git a/crates/temps-otel/src/ingest/rate_limit.rs b/crates/temps-otel/src/ingest/rate_limit.rs index b6ff5399..8b5a0a54 100644 --- a/crates/temps-otel/src/ingest/rate_limit.rs +++ b/crates/temps-otel/src/ingest/rate_limit.rs @@ -61,6 +61,13 @@ impl RateLimiter { true } + /// Maximum requests allowed per window per project. This is the value the + /// limiter was constructed with (from `TEMPS_OTEL_RATE_LIMIT`), and is the + /// single source of truth for the configured limit. + pub fn max_requests(&self) -> u32 { + self.max_requests + } + /// Get current count for a project (for observability). pub fn current_count(&self, project_id: i32) -> u32 { let counters = self.counters.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/crates/temps-otel/src/services/otel_service.rs b/crates/temps-otel/src/services/otel_service.rs index 0b36c091..18cdb68e 100644 --- a/crates/temps-otel/src/services/otel_service.rs +++ b/crates/temps-otel/src/services/otel_service.rs @@ -84,9 +84,11 @@ impl OtelService { /// Check rate limit for a project. pub fn check_rate_limit(&self, project_id: i32) -> Result<(), OtelError> { if !self.rate_limiter.check_and_increment(project_id) { + // Report the limiter's actual configured limit (set via + // `TEMPS_OTEL_RATE_LIMIT`) so the error matches reality. return Err(OtelError::RateLimitExceeded { project_id, - limit: 1000, // TODO: make configurable + limit: self.rate_limiter.max_requests(), }); } Ok(()) @@ -616,7 +618,12 @@ mod tests { assert!(svc.check_rate_limit(1).is_ok()); assert!(svc.check_rate_limit(1).is_ok()); let result = svc.check_rate_limit(1); - assert!(matches!(result, Err(OtelError::RateLimitExceeded { .. }))); + // The error must report the limiter's actual configured limit (2), + // not a hardcoded value. + assert!(matches!( + result, + Err(OtelError::RateLimitExceeded { limit: 2, .. }) + )); } #[tokio::test] diff --git a/crates/temps-proxy/src/proxy_test.rs b/crates/temps-proxy/src/proxy_test.rs index d7e7906d..63784591 100644 --- a/crates/temps-proxy/src/proxy_test.rs +++ b/crates/temps-proxy/src/proxy_test.rs @@ -387,6 +387,9 @@ pub mod proxy_tests { #[tokio::test] async fn test_proxy_visitor_management() -> Result<()> { + use sea_orm::{ActiveModelTrait, ActiveValue::Set}; + use temps_entities::{deployments, environments, projects}; + let test_db_mock = TestDatabase::with_migrations().await.unwrap(); let test_db = TestDBMockOperations::new(test_db_mock.connection_arc().clone()) .await @@ -399,18 +402,64 @@ pub mod proxy_tests { let visitor_manager = VisitorManagerImpl::new(test_db.db.clone(), crypto.clone(), ip_service); + // A visitor row has non-nullable project_id / environment_id, so + // get_or_create_visitor requires a real ProjectContext — create one. + let project = projects::ActiveModel { + slug: Set("visitor-mgmt-test".to_string()), + name: Set("Visitor Mgmt Test".to_string()), + repo_name: Set("visitor-app".to_string()), + repo_owner: Set("test-org".to_string()), + directory: Set("".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::Vite), + ..Default::default() + } + .insert(test_db.db.as_ref()) + .await?; + + let environment = environments::ActiveModel { + project_id: Set(project.id), + slug: Set("production".to_string()), + name: Set("Production".to_string()), + subdomain: Set("visitor-app".to_string()), + host: Set("visitor-app.example.com".to_string()), + upstreams: Set(temps_entities::upstream_config::UpstreamList::default()), + ..Default::default() + } + .insert(test_db.db.as_ref()) + .await?; + + let deployment = deployments::ActiveModel { + project_id: Set(project.id), + environment_id: Set(environment.id), + slug: Set("deploy-visitor-test".to_string()), + state: Set("deployed".to_string()), + metadata: Set(Some(Default::default())), + ..Default::default() + } + .insert(test_db.db.as_ref()) + .await?; + + let context = ProjectContext { + project: Arc::new(project), + environment: Arc::new(environment), + deployment: Arc::new(deployment), + }; + // Test visitor creation let attribution = crate::traits::FirstVisitAttribution::default(); let visitor = visitor_manager .get_or_create_visitor( None, // No existing cookie - None, // No project context + Some(&context), "Mozilla/5.0 (test)", Some("127.0.0.1"), &attribution, ) .await - .map_err(|_| anyhow::anyhow!("Failed to get or create visitor"))?; + // Surface the real error instead of swallowing it, so a future + // failure here is diagnosable. + .map_err(|e| anyhow::anyhow!("Failed to get or create visitor: {e}"))?; assert!(!visitor.visitor_id.is_empty()); assert!(!visitor.is_crawler); @@ -427,48 +476,111 @@ pub mod proxy_tests { assert!(cookie.contains("HttpOnly")); // Test bot detection - let bot_visitor = convert_send_sync_error( - visitor_manager - .get_or_create_visitor(None, None, "Googlebot/2.1", Some("127.0.0.1"), &attribution) - .await, - )?; + let bot_visitor = visitor_manager + .get_or_create_visitor( + None, + Some(&context), + "Googlebot/2.1", + Some("127.0.0.1"), + &attribution, + ) + .await + .map_err(|e| anyhow::anyhow!("Failed to get or create bot visitor: {e}"))?; assert!(bot_visitor.is_crawler); assert!(bot_visitor.crawler_name.is_some()); + // A request with no project context must be rejected, not silently + // create an orphan visitor. + let no_context = visitor_manager + .get_or_create_visitor( + None, + None, + "Mozilla/5.0 (test)", + Some("127.0.0.1"), + &attribution, + ) + .await; + assert!( + no_context.is_err(), + "visitor creation without project context must fail" + ); + // Note: Using shared database, so we don't cleanup individual test data Ok(()) } #[tokio::test] - #[ignore] // TODO: Fix foreign key constraint - needs visitor record creation before session async fn test_proxy_session_management() -> Result<()> { + use sea_orm::{ActiveModelTrait, ActiveValue::Set}; + use temps_entities::{environments, projects, visitor as visitor_entity}; + let _server_config = ProxyConfig::default(); let crypto = create_crypto_cookie_crypto(); let test_db_mock = TestDatabase::with_migrations().await.unwrap(); - let session_manager = - SessionManagerImpl::new(test_db_mock.connection_arc().clone(), crypto.clone()); + let db = test_db_mock.connection_arc().clone(); + let session_manager = SessionManagerImpl::new(db.clone(), crypto.clone()); + + // request_sessions.visitor_id has an FK to `visitor`, which in turn + // requires a project + environment — create the full chain so the + // session insert has a real visitor row to reference. + let project = projects::ActiveModel { + slug: Set("session-mgmt-test".to_string()), + name: Set("Session Mgmt Test".to_string()), + repo_name: Set("session-app".to_string()), + repo_owner: Set("test-org".to_string()), + directory: Set("".to_string()), + main_branch: Set("main".to_string()), + preset: Set(temps_entities::preset::Preset::Vite), + ..Default::default() + } + .insert(db.as_ref()) + .await?; + + let environment = environments::ActiveModel { + project_id: Set(project.id), + slug: Set("production".to_string()), + name: Set("Production".to_string()), + subdomain: Set("session-app".to_string()), + host: Set("session-app.example.com".to_string()), + upstreams: Set(temps_entities::upstream_config::UpstreamList::default()), + ..Default::default() + } + .insert(db.as_ref()) + .await?; + + let visitor_row = visitor_entity::ActiveModel { + visitor_id: Set("test-visitor-123".to_string()), + project_id: Set(project.id), + environment_id: Set(environment.id), + first_seen: Set(chrono::Utc::now()), + last_seen: Set(chrono::Utc::now()), + is_crawler: Set(false), + has_activity: Set(true), + ..Default::default() + } + .insert(db.as_ref()) + .await?; let visitor = Visitor { - visitor_id: "test-visitor-123".to_string(), - visitor_id_i32: 123, + visitor_id: visitor_row.visitor_id.clone(), + visitor_id_i32: visitor_row.id, is_crawler: false, crawler_name: None, }; // Test session creation - let session = convert_send_sync_error( - session_manager - .get_or_create_session( - None, // No existing cookie - &visitor, - None, // No project context - Some("https://example.com"), - None, // No query string - None, // No current hostname - ) - .await, - )?; + let session = session_manager + .get_or_create_session( + None, // No existing cookie + &visitor, + None, // No project context + Some("https://example.com"), + None, // No query string + None, // No current hostname + ) + .await + .map_err(|e| anyhow::anyhow!("Failed to get or create session: {e}"))?; assert!(!session.session_id.is_empty()); assert_eq!(session.visitor_id_i32, visitor.visitor_id_i32); diff --git a/crates/temps-proxy/src/services.rs b/crates/temps-proxy/src/services.rs index 9c9c5fe2..99066f90 100644 --- a/crates/temps-proxy/src/services.rs +++ b/crates/temps-proxy/src/services.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use temps_database::DbConnection; use temps_entities::{request_sessions, visitor}; use temps_routes::CachedPeerTable; -use tracing::{debug, error, warn}; +use tracing::{debug, warn}; use uuid::Uuid; const ROUTE_PREFIX_TEMPS: &str = "/api/_temps"; @@ -170,226 +170,6 @@ impl UpstreamResolver for UpstreamResolverImpl { } } -/// Implementation of RequestLogger trait -pub struct RequestLoggerImpl { - config: LoggingConfig, - db: Arc, - ip_service: Arc, -} - -impl RequestLoggerImpl { - pub fn new( - config: LoggingConfig, - db: Arc, - ip_service: Arc, - ) -> Self { - Self { - config, - db, - ip_service, - } - } -} - -#[async_trait] -impl RequestLogger for RequestLoggerImpl { - async fn log_request( - &self, - data: RequestLogData, - ) -> Result<(), Box> { - use sea_orm::{ActiveModelTrait, Set}; - use temps_entities::proxy_logs; - - // Skip logging if no project context - let Some(ref context) = data.project_context else { - debug!("Skipping request log - no project context"); - return Ok(()); - }; - - let elapsed_time = (data.finished_at - data.started_at).num_milliseconds() as i32; - - // Note: is_static_file and is_entry_page are not used in proxy_logs - // These were part of request_logs but proxy_logs doesn't track these fields - - // Parse user agent with woothee - let parser = woothee::parser::Parser::new(); - let ua_result = parser.parse(&data.user_agent); - - let (browser, browser_version, operating_system, is_mobile) = if let Some(ua) = ua_result { - let is_mob = ua.category == "smartphone" || ua.category == "mobilephone"; - ( - Some(ua.name.to_string()), - Some(ua.version.to_string()), - Some(ua.os.to_string()), - is_mob, - ) - } else { - (None, None, None, false) - }; - - // Get crawler info from visitor, or detect if not already detected - let (is_crawler, crawler_name) = if let Some(visitor) = data.visitor.as_ref() { - (visitor.is_crawler, visitor.crawler_name.clone()) - } else { - // Fall back to CrawlerDetector if visitor didn't detect it - let detected_crawler = CrawlerDetector::is_bot(Some(&data.user_agent)); - let detected_name = if detected_crawler { - CrawlerDetector::get_crawler_name(Some(&data.user_agent)) - } else { - None - }; - (detected_crawler, detected_name) - }; - - // Geolocate IP address - let ip_address_id = if let Some(ref ip) = data.ip_address { - match self.ip_service.get_or_create_ip(ip).await { - Ok(ip_info) => Some(ip_info.id), - Err(e) => { - warn!("Failed to geolocate IP {}: {:?}", ip, e); - None - } - } - } else { - None - }; - - // Clone values needed for debug logging before moving into ActiveModel - let method_clone = data.method.clone(); - let path_clone = data.path.clone(); - let status_code = data.status_code; - let visitor_id = data.visitor.as_ref().map(|v| v.visitor_id_i32); - let session_id = data.session.as_ref().map(|s| s.session_id_i32); - - // Determine routing status - let routing_status = if context.deployment.id > 0 { - "routed" - } else { - "no_deployment" - } - .to_string(); - - // Convert status_code to i16 - let status_code_i16 = data.status_code as i16; - - // Headers are already JSON values - let response_headers_json = data.response_headers; - let request_headers_json = data.request_headers; - - // Determine device type from is_mobile - let device_type = if is_mobile { - Some("mobile".to_string()) - } else { - Some("desktop".to_string()) - }; - - let log_entry = proxy_logs::ActiveModel { - timestamp: Set(data.started_at), - method: Set(data.method), - path: Set(data.path), - query_string: Set(None), // TODO: Extract query string from path if needed - host: Set(data.host), - status_code: Set(status_code_i16), - response_time_ms: Set(Some(elapsed_time)), - request_source: Set("proxy".to_string()), - is_system_request: Set(false), - routing_status: Set(routing_status), - project_id: Set(Some(context.project.id)), - environment_id: Set(Some(context.environment.id)), - deployment_id: Set(Some(context.deployment.id)), - container_id: Set(None), // TODO: Add container info if available - upstream_host: Set(None), // TODO: Add upstream host if available - error_message: Set(None), - client_ip: Set(data.ip_address), - user_agent: Set(Some(data.user_agent)), - referrer: Set(data.referrer), - request_id: Set(data.request_id), - ip_geolocation_id: Set(ip_address_id), - browser: Set(browser), - browser_version: Set(browser_version), - operating_system: Set(operating_system), - device_type: Set(device_type), - is_bot: Set(Some(is_crawler)), - bot_name: Set(crawler_name), - request_size_bytes: Set(None), // TODO: Add if available - response_size_bytes: Set(None), // TODO: Add if available - cache_status: Set(None), - request_headers: Set(Some(request_headers_json)), - response_headers: Set(Some(response_headers_json)), - created_date: Set(data.started_at.date_naive()), - session_id: Set(data.session.as_ref().map(|s| s.session_id_i32)), - visitor_id: Set(data.visitor.as_ref().map(|v| v.visitor_id_i32)), - trace_id: Set(data.trace_id), - error_group_id: Set(None), - ..Default::default() - }; - - match log_entry.insert(self.db.as_ref()).await { - Ok(_) => { - debug!( - "Request logged to DB: {} deployment_id={} {} - status: {}, visitor: {:?}, session: {:?}", - method_clone, - context.deployment.id, - &path_clone[..path_clone.len().min(50)], - status_code, - visitor_id, - session_id - ); - Ok(()) - } - Err(e) => { - error!("Failed to insert request log: {:?}", e); - Err(Box::new(e)) - } - } - } - - async fn log_error( - &self, - request_id: &str, - host: &str, - path: &str, - error: &str, - _context: Option<&ProjectContext>, - ) -> Result<(), Box> { - error!( - "Request error [{}] {}{} - {}", - request_id, host, path, error - ); - Ok(()) - } - - async fn should_log_request(&self, _context: Option<&ProjectContext>) -> bool { - self.config.log_all_requests - } -} - -/// Configuration for request logging -#[derive(Debug, Clone)] -pub struct LoggingConfig { - pub log_all_requests: bool, - pub log_static_assets: bool, - pub log_internal_api: bool, - pub log_non_project_requests: bool, - pub log_request_headers: bool, - pub log_response_headers: bool, - pub max_header_size: usize, -} - -impl Default for LoggingConfig { - fn default() -> Self { - Self { - log_all_requests: true, - log_static_assets: false, - log_internal_api: false, - log_non_project_requests: true, - log_request_headers: true, - log_response_headers: true, - max_header_size: 16 * 1024, - } - } -} - /// Implementation of ProjectContextResolver trait pub struct ProjectContextResolverImpl { route_table: Arc, @@ -830,8 +610,7 @@ mod tests { use temps_database::test_utils::TestDatabase; use temps_entities::{ - deployments, environments, preset::Preset, projects, proxy_logs, - upstream_config::UpstreamList, visitor, + deployments, environments, preset::Preset, projects, upstream_config::UpstreamList, visitor, }; fn create_mock_ip_service(db: Arc) -> Arc { @@ -864,28 +643,6 @@ mod tests { visitor.id } - async fn create_test_session( - db: &Arc, - session_id: &str, - visitor_id_i32: i32, - ) -> i32 { - use chrono::Utc; - use sea_orm::ActiveValue::Set; - use temps_entities::request_sessions; - - let session_model = request_sessions::ActiveModel { - session_id: Set(session_id.to_string()), - started_at: Set(Utc::now()), - last_accessed_at: Set(Utc::now()), - visitor_id: Set(Some(visitor_id_i32)), - data: Set("{}".to_string()), - ..Default::default() - }; - - let session = session_model.insert(db.as_ref()).await.unwrap(); - session.id - } - async fn create_test_project_context(db: &Arc) -> ProjectContext { // Create test project let project = projects::ActiveModel { @@ -932,292 +689,6 @@ mod tests { } } - #[tokio::test] - async fn test_request_logger_user_agent_parsing() { - let test_db = TestDatabase::with_migrations().await.unwrap(); - let ip_service = create_mock_ip_service(test_db.connection_arc().clone()); - let logger = RequestLoggerImpl::new( - LoggingConfig::default(), - test_db.connection_arc().clone(), - ip_service, - ); - - let context = create_test_project_context(&test_db.connection_arc()).await; - - // Test Chrome user agent - let chrome_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; - let log_data = RequestLogData { - request_id: "test-req-1".to_string(), - host: "test.example.com".to_string(), - method: "GET".to_string(), - path: "/test".to_string(), - status_code: 200, - user_agent: chrome_ua.to_string(), - referrer: None, - ip_address: Some("8.8.8.8".to_string()), - started_at: chrono::Utc::now(), - finished_at: chrono::Utc::now(), - request_headers: serde_json::json!({}), - response_headers: serde_json::json!({}), - visitor: None, - session: None, - project_context: Some(context.clone()), - trace_id: None, - }; - - logger.log_request(log_data).await.unwrap(); - - // Verify log was created with parsed user agent data - let logs = proxy_logs::Entity::find() - .filter(proxy_logs::Column::RequestId.eq("test-req-1")) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("Log should be created"); - - assert_eq!(logs.browser, Some("Chrome".to_string())); - assert!(logs.browser_version.is_some()); - assert_eq!(logs.operating_system, Some("Windows 10".to_string())); - assert_ne!(logs.device_type, Some("mobile".to_string())); - } - - #[tokio::test] - async fn test_request_logger_mobile_detection() { - let test_db = TestDatabase::with_migrations().await.unwrap(); - let ip_service = create_mock_ip_service(test_db.connection_arc().clone()); - let logger = RequestLoggerImpl::new( - LoggingConfig::default(), - test_db.connection_arc().clone(), - ip_service, - ); - - let context = create_test_project_context(&test_db.connection_arc()).await; - - // Test mobile Safari user agent - let mobile_ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; - let log_data = RequestLogData { - request_id: "test-req-mobile".to_string(), - host: "test.example.com".to_string(), - method: "GET".to_string(), - path: "/test".to_string(), - status_code: 200, - user_agent: mobile_ua.to_string(), - referrer: None, - ip_address: Some("1.2.3.4".to_string()), - started_at: chrono::Utc::now(), - finished_at: chrono::Utc::now(), - request_headers: serde_json::json!({}), - response_headers: serde_json::json!({}), - visitor: None, - session: None, - project_context: Some(context), - trace_id: None, - }; - - logger.log_request(log_data).await.unwrap(); - - // Verify mobile detection - let logs = proxy_logs::Entity::find() - .filter(proxy_logs::Column::RequestId.eq("test-req-mobile")) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("Log should be created"); - - assert_eq!(logs.device_type, Some("mobile".to_string())); - assert_eq!(logs.operating_system, Some("iPhone".to_string())); - } - - #[tokio::test] - async fn test_request_logger_crawler_detection() { - let test_db = TestDatabase::with_migrations().await.unwrap(); - let ip_service = create_mock_ip_service(test_db.connection_arc().clone()); - let logger = RequestLoggerImpl::new( - LoggingConfig::default(), - test_db.connection_arc().clone(), - ip_service, - ); - - let context = create_test_project_context(&test_db.connection_arc()).await; - - // Test Googlebot user agent - let bot_ua = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"; - let log_data = RequestLogData { - request_id: "test-req-bot".to_string(), - host: "test.example.com".to_string(), - method: "GET".to_string(), - path: "/test".to_string(), - status_code: 200, - user_agent: bot_ua.to_string(), - referrer: None, - ip_address: None, - started_at: chrono::Utc::now(), - finished_at: chrono::Utc::now(), - request_headers: serde_json::json!({}), - response_headers: serde_json::json!({}), - visitor: None, - session: None, - project_context: Some(context), - trace_id: None, - }; - - logger.log_request(log_data).await.unwrap(); - - // Verify crawler detection - let logs = proxy_logs::Entity::find() - .filter(proxy_logs::Column::RequestId.eq("test-req-bot")) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("Log should be created"); - - assert_eq!(logs.is_bot, Some(true)); - assert!(logs.bot_name.is_some()); - assert!(logs.bot_name.unwrap().contains("Google")); - } - - #[tokio::test] - async fn test_request_logger_ip_geolocation() { - let test_db = TestDatabase::with_migrations().await.unwrap(); - let ip_service = create_mock_ip_service(test_db.connection_arc().clone()); - let logger = RequestLoggerImpl::new( - LoggingConfig::default(), - test_db.connection_arc().clone(), - ip_service.clone(), - ); - - let context = create_test_project_context(&test_db.connection_arc()).await; - - // Test with a real IP address - let test_ip = "8.8.8.8"; // Google DNS - let log_data = RequestLogData { - request_id: "test-req-ip".to_string(), - host: "test.example.com".to_string(), - method: "GET".to_string(), - path: "/test".to_string(), - status_code: 200, - user_agent: "Mozilla/5.0".to_string(), - referrer: None, - ip_address: Some(test_ip.to_string()), - started_at: chrono::Utc::now(), - finished_at: chrono::Utc::now(), - request_headers: serde_json::json!({}), - response_headers: serde_json::json!({}), - visitor: None, - session: None, - project_context: Some(context), - trace_id: None, - }; - - logger.log_request(log_data).await.unwrap(); - - // Verify IP geolocation was created - let logs = proxy_logs::Entity::find() - .filter(proxy_logs::Column::RequestId.eq("test-req-ip")) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("Log should be created"); - - assert!( - logs.ip_geolocation_id.is_some(), - "IP address should be geolocated" - ); - assert_eq!(logs.client_ip, Some(test_ip.to_string())); - - // Verify the IP address record was created with geolocation data - let ip_record = - temps_entities::ip_geolocations::Entity::find_by_id(logs.ip_geolocation_id.unwrap()) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("IP address record should exist"); - - assert_eq!(ip_record.ip_address, test_ip); - // Country should be populated by the geolocation service (country is required field) - assert!(!ip_record.country.is_empty()); - } - - #[tokio::test] - async fn test_request_logger_with_visitor_and_session() { - let test_db = TestDatabase::with_migrations().await.unwrap(); - let ip_service = create_mock_ip_service(test_db.connection_arc().clone()); - let logger = RequestLoggerImpl::new( - LoggingConfig::default(), - test_db.connection_arc().clone(), - ip_service, - ); - - let context = create_test_project_context(&test_db.connection_arc()).await; - - // Create visitor record in database first - let visitor_id_i32 = create_test_visitor( - &test_db.connection_arc(), - "test-visitor-123", - context.project.id, - context.environment.id, - ) - .await; - - // Create session record in database - let session_id_i32 = create_test_session( - &test_db.connection_arc(), - "test-session-456", - visitor_id_i32, - ) - .await; - - // Create test visitor - let visitor_data = Visitor { - visitor_id: "test-visitor-123".to_string(), - visitor_id_i32, - is_crawler: false, - crawler_name: None, - }; - - // Create test session - let session_data = Session { - session_id: "test-session-456".to_string(), - session_id_i32, - visitor_id_i32, - is_new_session: true, - }; - - let log_data = RequestLogData { - request_id: "test-req-with-visitor".to_string(), - host: "test.example.com".to_string(), - method: "GET".to_string(), - path: "/test".to_string(), - status_code: 200, - user_agent: "Mozilla/5.0".to_string(), - referrer: Some("https://google.com".to_string()), - ip_address: Some("1.2.3.4".to_string()), - started_at: chrono::Utc::now(), - finished_at: chrono::Utc::now(), - request_headers: serde_json::json!({}), - response_headers: serde_json::json!({}), - visitor: Some(visitor_data), - session: Some(session_data), - project_context: Some(context), - trace_id: None, - }; - - logger.log_request(log_data).await.unwrap(); - - // Verify visitor and session IDs are stored - let logs = proxy_logs::Entity::find() - .filter(proxy_logs::Column::RequestId.eq("test-req-with-visitor")) - .one(test_db.connection_arc().as_ref()) - .await - .unwrap() - .expect("Log should be created"); - - assert_eq!(logs.visitor_id, Some(visitor_id_i32)); - assert_eq!(logs.session_id, Some(session_id_i32)); - // Note: proxy_logs doesn't track is_entry_page like request_logs did - assert_eq!(logs.referrer, Some("https://google.com".to_string())); - } - #[tokio::test] async fn test_session_creation_and_reuse() { let test_db = TestDatabase::with_migrations().await.unwrap(); diff --git a/crates/temps-proxy/src/traits.rs b/crates/temps-proxy/src/traits.rs index d71acd8c..0ed3d7b1 100644 --- a/crates/temps-proxy/src/traits.rs +++ b/crates/temps-proxy/src/traits.rs @@ -2,7 +2,6 @@ use async_trait::async_trait; use pingora_core::{upstreams::peer::HttpPeer, Result as PingoraResult}; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use temps_core::UtcDateTime; use temps_entities::{deployments, environments, projects}; /// Context information about a request's project, environment, and deployment @@ -31,31 +30,6 @@ pub struct Session { pub is_new_session: bool, } -/// Request metadata for logging -#[derive(Debug, Clone, Serialize)] -pub struct RequestLogData { - pub request_id: String, - pub host: String, - pub method: String, - pub path: String, - pub status_code: i32, - pub user_agent: String, - pub referrer: Option, - pub ip_address: Option, - pub started_at: UtcDateTime, - pub finished_at: UtcDateTime, - pub request_headers: serde_json::Value, - pub response_headers: serde_json::Value, - pub visitor: Option, - pub session: Option, - pub project_context: Option, - /// W3C `traceparent` trace_id (32 hex chars) extracted from the inbound - /// request headers when present. Stamped onto `proxy_logs.trace_id` so - /// the unified Observe view can correlate this request with its child - /// spans, runtime logs, and any captured exceptions. - pub trace_id: Option, -} - /// Cookie configuration for visitor/session tracking #[derive(Debug, Clone)] pub struct CookieConfig { @@ -112,29 +86,6 @@ pub trait UpstreamResolver: Send + Sync { async fn get_lb_strategy(&self, host: &str) -> Option; } -/// Trait for logging request/response data -#[async_trait] -pub trait RequestLogger: Send + Sync { - /// Log a completed request with all metadata - async fn log_request( - &self, - data: RequestLogData, - ) -> Result<(), Box>; - - /// Log an error that occurred during request processing - async fn log_error( - &self, - request_id: &str, - host: &str, - path: &str, - error: &str, - context: Option<&ProjectContext>, - ) -> Result<(), Box>; - - /// Check if logging is enabled for a specific project/environment - async fn should_log_request(&self, context: Option<&ProjectContext>) -> bool; -} - /// Trait for resolving project context from request information #[async_trait] pub trait ProjectContextResolver: Send + Sync { diff --git a/sdks/python/uv.lock b/sdks/python/uv.lock index 96c2c1cb..3dc754b1 100644 --- a/sdks/python/uv.lock +++ b/sdks/python/uv.lock @@ -218,11 +218,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] diff --git a/web/public/favicon/temps-favicon-16x16.png b/web/public/favicon/temps-favicon-16x16.png index b37bd66c..82895621 100644 Binary files a/web/public/favicon/temps-favicon-16x16.png and b/web/public/favicon/temps-favicon-16x16.png differ diff --git a/web/public/favicon/temps-favicon-180x180.png b/web/public/favicon/temps-favicon-180x180.png index ecb3101f..670a97c4 100644 Binary files a/web/public/favicon/temps-favicon-180x180.png and b/web/public/favicon/temps-favicon-180x180.png differ diff --git a/web/public/favicon/temps-favicon-192x192.png b/web/public/favicon/temps-favicon-192x192.png index 6f7340ff..748b0f0c 100644 Binary files a/web/public/favicon/temps-favicon-192x192.png and b/web/public/favicon/temps-favicon-192x192.png differ diff --git a/web/public/favicon/temps-favicon-32x32.png b/web/public/favicon/temps-favicon-32x32.png index 3274b058..4204b305 100644 Binary files a/web/public/favicon/temps-favicon-32x32.png and b/web/public/favicon/temps-favicon-32x32.png differ diff --git a/web/public/favicon/temps-favicon-48x48.png b/web/public/favicon/temps-favicon-48x48.png index c764ad37..446fa86b 100644 Binary files a/web/public/favicon/temps-favicon-48x48.png and b/web/public/favicon/temps-favicon-48x48.png differ diff --git a/web/public/favicon/temps-favicon-512x512.png b/web/public/favicon/temps-favicon-512x512.png index 37759a81..36bddd30 100644 Binary files a/web/public/favicon/temps-favicon-512x512.png and b/web/public/favicon/temps-favicon-512x512.png differ diff --git a/web/public/icon/temps-icon-512.png b/web/public/icon/temps-icon-512.png index 37759a81..36bddd30 100644 Binary files a/web/public/icon/temps-icon-512.png and b/web/public/icon/temps-icon-512.png differ diff --git a/web/public/svg/temps-icon-dark.svg b/web/public/svg/temps-icon-dark.svg index b7212623..933f1c18 100644 --- a/web/public/svg/temps-icon-dark.svg +++ b/web/public/svg/temps-icon-dark.svg @@ -1,45 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{"signed_by": "recraft", "signature_b64": "IsbuWIFZYDDn0pLE/G4tBNiuFECghrRtDz1yaxknRQK81hkEH0/sKuV9cVAmyvM6XmvCdnt2M/udwslWIFTVCw==", "signing_algo": "Ed25519", "generation_timestamp": 1768310680, "identifier": "041031dc-320a-41c3-9223-38edb637ba4c"} - - + + + t diff --git a/web/public/svg/temps-icon.svg b/web/public/svg/temps-icon.svg index b7212623..933f1c18 100644 --- a/web/public/svg/temps-icon.svg +++ b/web/public/svg/temps-icon.svg @@ -1,45 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{"signed_by": "recraft", "signature_b64": "IsbuWIFZYDDn0pLE/G4tBNiuFECghrRtDz1yaxknRQK81hkEH0/sKuV9cVAmyvM6XmvCdnt2M/udwslWIFTVCw==", "signing_algo": "Ed25519", "generation_timestamp": 1768310680, "identifier": "041031dc-320a-41c3-9223-38edb637ba4c"} - - + + + t diff --git a/web/src/api/client/@tanstack/react-query.gen.ts b/web/src/api/client/@tanstack/react-query.gen.ts index e43ff807..77dac087 100644 --- a/web/src/api/client/@tanstack/react-query.gen.ts +++ b/web/src/api/client/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, getProxyLogByRequestId, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; +import { type Options, getPlatformInfo, chunkUploadOptions, createRelease, createProjectRelease, finalizeProjectRelease, listReleaseFiles, uploadReleaseFile, recordEventMetrics, addSessionReplayEvents, initSessionReplay, recordSpeedMetrics, updateSpeedMetrics, webhookTrigger, getPricing, listProviderKeys, createProviderKey, testProviderKeyInline, deleteProviderKey, updateProviderKey, testProviderKeyById, getUsageByProvider, getConversations, getConversationDetail, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, chatCompletions, embeddings, listModels, getAnalyticsActiveVisitors, getEventDetail, getEventVisitors, getAnalyticsEventsCount, getGeneralStats, getLiveVisitorsList, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePathVisitors, getPagePaths, getPagePathsSparklines, getRecentActivity, getSessionDetails, getAnalyticsSessionEvents, getSessionLogs, getVisitorFacets, getVisitors, getVisitorByGuid, getVisitorById, getVisitorDetails, enrichVisitor, getVisitorInfo, getVisitorJourney, getAnalyticsVisitorSessions, getVisitorStats, listApiKeys, createApiKey, getApiKeyPermissions, deleteApiKey, getApiKey, updateApiKey, activateApiKey, deactivateApiKey, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, emailStatus, login, requestMagicLink, verifyMagicLink, requestPasswordReset, resetPassword, verifyEmail, verifyMfaChallenge, listBackupAlerts, runExternalServiceBackup, listExternalServiceBackups, listServiceSchedules, listS3Sources, createS3Source, testS3ConnectionPreview, deleteS3Source, getS3Source, updateS3Source, listSourceBackups, runBackupForSource, setDefaultS3Source, testS3SourceConnection, cancelScheduleRun, listScheduleRunJobs, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, getBackupSchedule, updateBackupSchedule, listBackupsForSchedule, disableBackupSchedule, enableBackupSchedule, runScheduleNow, listScheduleRuns, listScheduleServices, attachScheduleServices, detachScheduleService, getBackup, cancelBackup, listBackupChildren, blobDelete, blobList, blobPut, blobCopy, blobDisable, blobEnable, blobStatus, blobUpdate, blobDownload, getDashboardProjectsAnalytics, getActivityGraph, getScanByDeployment, listDnsProviders, createDnsProvider, deleteDnsProvider, getDnsProvider, updateProvider, listManagedDomains, addManagedDomain, testProviderConnection, listProviderZones, removeManagedDomain, verifyManagedDomain, lookupDnsARecords, listDomains, createDomain, getDomainByHost, cancelDomainOrder, getDomainOrder, createOrRecreateOrder, finalizeOrder, setupDnsChallenge, deleteDomain, getDomainById, getChallengeToken, getHttpChallengeDebug, provisionDomain, renewDomain, checkDomainStatus, listEmailDomains, createEmailDomain, getDomainByName, deleteEmailDomain, getDomain, getDomainDnsRecords, setupDns, verifyDomain, listEmailProviders, createEmailProvider, deleteEmailProvider, getEmailProvider, testProvider, listEmails, sendEmail, getGlobalEvents, getGlobalEventStats, getEmailStats, validateEmail, trackClick, trackOpen, getEmail, getEmailTracking, getEmailEvents, getEmailLinks, listServices, createService, listAvailableContainers, getServiceBySlug, listServiceHealthStatuses, importExternalService, listProjectServices, getProjectServiceEnvironmentVariables, getProvidersMetadata, getProviderMetadata, getServiceTypes, getServiceTypeParameters, deleteService, getService, updateService, getClusterHealth, triggerServiceHealthCheck, getServiceHealthStatus, addClusterMember, removeClusterMember, getClusterMember, promoteClusterMember, getServicePreviewEnvironmentVariablesMasked, getServicePreviewEnvironmentVariableNames, listServiceProjects, linkServiceToProject, unlinkServiceFromProject, getServiceEnvironmentVariables, getServiceEnvironmentVariable, updateServiceResources, startRestore, getRestoreCapabilities, planRestore, listRestoreRunsForService, retryCluster, getServiceRuntime, startService, getServiceStats, stopService, upgradeService, getPostgresWalHealth, listRootContainers, listContainersAtPath, listEntities, getEntityInfo, queryData, downloadObject, getContainerInfo, checkExplorerSupport, listPgUpgrades, startPgUpgrade, getPgUpgrade, cancelPgUpgrade, getPgUpgradeLogs, retryPgUpgrade, rollbackPgUpgrade, getFile, getIpGeolocation, listConnections, deleteConnection, activateConnection, deactivateConnection, runConnectionHealthCheck, listRepositoriesByConnection, syncRepositories, updateConnectionToken, validateConnection, listGitProviders, createGitProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, deleteGitProvider, getGitProvider, activateProvider, handleGitProviderOauthCallback, getProviderConnections, updateGitProviderCredentials, deactivateProvider, checkProviderDeletionSafety, startGitProviderOauth, deleteProviderSafely, getPublicRepository, getPublicBranches, detectPublicPresets, discoverWorkloads, executeImport, createPlan, listSources, getImportStatus, getIncident, updateIncidentStatus, getIncidentUpdates, adminListNodes, registerNode, adminRemoveNode, adminGetNode, adminListNodeContainers, postDnsAck, getDnsChanges, adminUndrainNode, adminDrainStatus, adminDrainNode, nodeHeartbeat, listPeers, getS3Credentials, listIpAccessControl, createIpAccessControl, checkIpBlocked, deleteIpAccessControl, getIpAccessControl, updateIpAccessControl, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, listRoutes, createRoute, deleteRoute, getRoute, updateRoute, logout, getLogContext, searchLogs, tailLogs, getProjectsMonitorHealth, deleteMonitor, getMonitor, getBucketedStatus, getCurrentMonitorStatus, getUptimeHistory, deletePreferences, getPreferences, updatePreferences, listNotificationProviders, createNotificationProvider, createNotificationEmailProvider, updateEmailProvider, createSlackProvider, updateSlackProvider, createWebhookProvider, updateWebhookProvider, deleteNotificationProvider, getNotificationProvider, updateNotificationProvider, testNotificationProvider, listOrders, queryGenaiTraces, getGenaiTrace, getHealth, listInsights, queryLogs, listMetricNames, queryMetrics, getPipelineStats, getQuota, queryTraceSummaries, queryTraces, getTrace, ingestLogs, ingestMetrics, ingestTraces, ingestLogsByPath, ingestMetricsByPath, ingestTracesByPath, hasPerformanceMetrics, getPerformanceMetrics, getMetricsOverTime, getGroupedPageMetrics, getAccessInfo, getPrivateIp, getPublicIp, listPresets, generatePresetDockerfile, getPreviewGatewayLogs, restartPreviewGateway, getPreviewGatewaySettings, patchPreviewGatewaySettings, getPreviewGatewayStatus, upgradePreviewGateway, getProjects, createProject, getProjectBySlug, createProjectFromTemplate, getProjectStatistics, deleteProject, getProject, updateProject, getProjectDeployments, getLastDeployment, triggerProjectPipeline, getActiveVisitors, listAgents, createAgent, getCliStatus, listAllRuns, latestRunForSource, getRunWithLogs, cancelRun, retryRun, getSandboxStatus, smokeTestAgent, deleteAgent, getAgent, updateAgent, listAgentRuns, triggerAgent, getAggregatedBuckets, startAnalysis, getRun, addContext, cancel, createPr, startFix, reAnalyze, updateAutomaticDeploy, listCustomDomainsForProject, createCustomDomain, deleteCustomDomain, getCustomDomain, updateCustomDomain, linkCustomDomainToCertificate, updateProjectDeploymentConfig, getDeployment, cancelDeployment, getDeploymentJobs, getDeploymentJobLogs, tailDeploymentJobLogs, getDeploymentOperations, executeDeploymentOperation, getDeploymentOperationStatus, pauseDeployment, promoteDeployment, resumeDeployment, rollbackToDeployment, teardownDeployment, listDsns, createDsn, getOrCreateDsn, regenerateDsn, revokeDsn, getEnvironmentVariables, createEnvironmentVariable, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getEnvironmentVariableValue, deleteEnvironmentVariable, updateEnvironmentVariable, getEnvironments, createEnvironment, deleteEnvironment, getEnvironment, getEnvironmentCrons, getCronById, getCronExecutions, getEnvironmentDomains, addEnvironmentDomain, deleteEnvironmentDomain, updateEnvironmentSettings, sleepEnvironment, updateEnvironmentSubdomain, teardownEnvironment, wakeEnvironment, getContainerLogs, listContainers, getContainerDetail, getContainerLogsById, getContainerMetrics, streamContainerMetrics, restartContainer, startContainer, stopContainer, deployFromImage, deployFromImageUpload, deployFromStatic, listAlertRules, createAlertRule, deleteAlertRule, getAlertRule, updateAlertRule, getErrorDashboardStats, listErrorGroups, getErrorGroup, updateErrorGroup, listErrorEvents, getErrorEvent, getErrorStats, getErrorTimeSeries, getEventsCount, getEventTypeBreakdown, recordConsoleEvent, getPropertyBreakdown, getPropertyTimeline, getEventsTimeline, getUniqueEvents, listRemoteExternalImages, registerExternalImage, deleteExternalImage, getRemoteExternalImage, listFunnels, createFunnel, previewFunnelMetrics, deleteFunnel, updateFunnel, getFunnelMetrics, updateGitSettings, reinstallGitlabWebhook, hasErrorGroups, hasAnalyticsEvents, getHourlyVisits, listExternalImages, pushExternalImage, getExternalImage, listIncidents, createIncident, getBucketedIncidents, purgeProjectLogs, listMcps, createMcp, deleteMcp, getMcp, updateMcp, listMonitors, createMonitor, observabilityListEvents, observabilityFullEvent, deleteReleaseSourceMaps, listSourceMaps, uploadSourceMap, revenueRecentEvents, revenueListIntegrations, revenueCreateIntegration, revenueDeleteIntegration, revenueUpdateConfig, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueRotateToken, revenueUpdateSecret, revenueMetricsCustomers, revenueMetricsMrr, revenueMetricsSummary, listProjectSecrets, createProjectSecret, deleteProjectSecret, updateProjectSecret, updateProjectSettings, listSkills, createSkill, uploadSkill, deleteSkill, getSkill, updateSkill, downloadSkillArchive, listReleases, deleteSourceMap, listStaticBundles, deleteStaticBundle, getStaticBundle, getStatusOverview, getUniqueCounts, uploadStaticBundle, listProjectScans, triggerScan, getLatestScansPerEnvironment, getLatestScan, listWebhooks, createWebhook, deleteWebhook, getWebhook, updateWebhook, listDeliveries, getDelivery, retryDelivery, workflowDryRun, getProxyLogs, getProxyLogByRequestId, getProjectsHealth, getTimeBucketStats, getTodayStats, getProxyLogById, listSyncedRepositories, getRepositoryByName, getAllRepositoriesByName, getRepositoryPresetByName, getRepositoryBranches, getRepositoryTags, getRepositoryPresetLive, getRepositoryById, getBranchesByRepositoryId, listCommitsByRepositoryId, checkCommitExists, getTagsByRepositoryId, getRestoreRun, revenueGlobalEvents, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueListProviders, getProjectSessionReplays, getSessionEvents, getSettings, updateSettings, saveAgentToken, listAiProviders, updateAiProvider, activateAiProvider, saveAiProviderCredential, revokeJoinToken, generateJoinToken, getJoinTokenStatus, listGlobalMcps, createGlobalMcp, deleteGlobalMcp, getGlobalMcp, updateGlobalMcp, refreshRouteTable, rebuildSandboxImage, getGlobalSandboxStatus, listSecrets, upsertSecret, deleteSecret, listGlobalSkills, createGlobalSkill, uploadGlobalSkill, deleteGlobalSkill, getGlobalSkill, updateGlobalSkill, downloadGlobalSkillArchive, listProjectTemplates, listProjectTemplateTags, getProjectTemplate, getCurrentUser, listUsers, createUser, updateSelf, disableMfa, setupMfa, verifyAndEnableMfa, changePasswordSelf, deleteUser, updateUser, restoreUser, assignRole, removeRole, listSandboxes, createSandbox, getSandbox, cmd, getCmd, cmdLogs, destroySandbox, domain, exec, execDetached, extendTimeout, mkdir, readFile, statPath, writeFile, writeFiles, listJobs, jobStatus, killJob, jobLogs, pauseSandbox, clearPreviewPassword, setPreviewPassword, restartSandbox, resumeSandbox, sourceSandbox, stopSandbox, cmdKill, getVisitorSessions, deleteSessionReplay, getSessionReplay, updateSessionDuration, getSessionReplayEvents, addEvents, deleteScan, getScan, getScanVulnerabilities, listEventTypes, triggerWeeklyDigest, listExternalPlugins, reloadPlugins, ingestSentryEnvelope, ingestSentryEvent, listAuditLogs, getAuditLog } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query'; -import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProviderData, UpdateEmailProviderResponse, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, GetProxyLogByRequestIdData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; +import type { GetPlatformInfoData, ChunkUploadOptionsData, CreateReleaseData, CreateReleaseResponse, CreateProjectReleaseData, CreateProjectReleaseResponse, FinalizeProjectReleaseData, FinalizeProjectReleaseResponse, ListReleaseFilesData, UploadReleaseFileData, UploadReleaseFileResponse, RecordEventMetricsData, RecordEventMetricsResponse, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsResponse, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsResponse, WebhookTriggerData, WebhookTriggerResponse2 as WebhookTriggerResponse, GetPricingData, ListProviderKeysData, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyResponse, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineResponse, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyResponse, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyResponse, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdResponse, GetUsageByProviderData, GetConversationsData, GetConversationDetailData, GetUsageRecentData, GetUsageSummaryData, GetUsageTimeseriesData, GetUsageTopModelsData, ChatCompletionsData, ChatCompletionsError, ChatCompletionsResponse, EmbeddingsData, EmbeddingsError, EmbeddingsResponse, ListModelsData, GetAnalyticsActiveVisitorsData, GetEventDetailData, GetEventVisitorsData, GetEventVisitorsResponse, GetAnalyticsEventsCountData, GetGeneralStatsData, GetLiveVisitorsListData, GetPageFlowData, GetPageHourlySessionsData, GetPagePathDetailData, GetPagePathVisitorsData, GetPagePathVisitorsResponse, GetPagePathsData, GetPagePathsSparklinesData, GetRecentActivityData, GetSessionDetailsData, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponse, GetSessionLogsData, GetSessionLogsResponse, GetVisitorFacetsData, GetVisitorsData, GetVisitorsResponse, GetVisitorByGuidData, GetVisitorByIdData, GetVisitorDetailsData, EnrichVisitorData, EnrichVisitorResponse2 as EnrichVisitorResponse, GetVisitorInfoData, GetVisitorJourneyData, GetAnalyticsVisitorSessionsData, GetVisitorStatsData, ListApiKeysData, ListApiKeysResponse, CreateApiKeyData, CreateApiKeyResponse2 as CreateApiKeyResponse, GetApiKeyPermissionsData, DeleteApiKeyData, DeleteApiKeyResponse, GetApiKeyData, UpdateApiKeyData, UpdateApiKeyResponse, ActivateApiKeyData, ActivateApiKeyResponse, DeactivateApiKeyData, DeactivateApiKeyResponse, CliDeviceApproveData, CliDeviceApproveResponse2 as CliDeviceApproveResponse, CliDeviceDenyData, CliDeviceDenyResponse, CliDeviceLookupData, CliDevicePollData, CliDevicePollResponse2 as CliDevicePollResponse, CliDeviceStartData, CliDeviceStartResponse2 as CliDeviceStartResponse, CliLogoutData, CliLogoutResponse, EmailStatusData, LoginData, LoginResponse, RequestMagicLinkData, RequestMagicLinkResponse, VerifyMagicLinkData, RequestPasswordResetData, RequestPasswordResetResponse, ResetPasswordData, ResetPasswordResponse, VerifyEmailData, VerifyMfaChallengeData, VerifyMfaChallengeResponse, ListBackupAlertsData, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupResponse, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsResponse, ListServiceSchedulesData, ListS3SourcesData, CreateS3SourceData, CreateS3SourceError, CreateS3SourceResponse, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewResponse, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceResponse, GetS3SourceData, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceResponse, ListSourceBackupsData, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceResponse, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceResponse, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionResponse, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunResponse, ListScheduleRunJobsData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleResponse, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleResponse, GetBackupScheduleData, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleResponse, ListBackupsForScheduleData, DisableBackupScheduleData, DisableBackupScheduleResponse, EnableBackupScheduleData, EnableBackupScheduleResponse, RunScheduleNowData, RunScheduleNowError, RunScheduleNowResponse, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsResponse, ListScheduleServicesData, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesResponse2 as AttachScheduleServicesResponse, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceResponse, GetBackupData, CancelBackupData, CancelBackupError, CancelBackupResponse2 as CancelBackupResponse, ListBackupChildrenData, BlobDeleteData, BlobDeleteError, BlobDeleteResponse, BlobListData, BlobListError, BlobListResponse, BlobPutData, BlobPutError, BlobPutResponse, BlobCopyData, BlobCopyError, BlobCopyResponse, BlobDisableData, BlobDisableResponse, BlobEnableData, BlobEnableResponse, BlobStatusData, BlobUpdateData, BlobUpdateResponse, BlobDownloadData, GetDashboardProjectsAnalyticsData, GetActivityGraphData, GetScanByDeploymentData, ListDnsProvidersData, CreateDnsProviderData, CreateDnsProviderResponse, DeleteDnsProviderData, DeleteDnsProviderResponse, GetDnsProviderData, UpdateProviderData, UpdateProviderResponse, ListManagedDomainsData, AddManagedDomainData, AddManagedDomainResponse, TestProviderConnectionData, TestProviderConnectionResponse, ListProviderZonesData, RemoveManagedDomainData, RemoveManagedDomainResponse, VerifyManagedDomainData, VerifyManagedDomainResponse, LookupDnsARecordsData, ListDomainsData, ListDomainsResponse2 as ListDomainsResponse, CreateDomainData, CreateDomainResponse, GetDomainByHostData, CancelDomainOrderData, CancelDomainOrderResponse, GetDomainOrderData, CreateOrRecreateOrderData, CreateOrRecreateOrderResponse, FinalizeOrderData, FinalizeOrderResponse, SetupDnsChallengeData, SetupDnsChallengeResponse2 as SetupDnsChallengeResponse, DeleteDomainData, DeleteDomainResponse, GetDomainByIdData, GetChallengeTokenData, GetHttpChallengeDebugData, ProvisionDomainData, ProvisionDomainResponse, RenewDomainData, RenewDomainResponse, CheckDomainStatusData, ListEmailDomainsData, CreateEmailDomainData, CreateEmailDomainResponse, GetDomainByNameData, DeleteEmailDomainData, DeleteEmailDomainResponse, GetDomainData, GetDomainDnsRecordsData, SetupDnsData, SetupDnsResponse2 as SetupDnsResponse, VerifyDomainData, VerifyDomainResponse, ListEmailProvidersData, CreateEmailProviderData, CreateEmailProviderResponse, DeleteEmailProviderData, DeleteEmailProviderResponse, GetEmailProviderData, TestProviderData, TestProviderResponse2 as TestProviderResponse, ListEmailsData, ListEmailsResponse, SendEmailData, SendEmailResponse, GetGlobalEventsData, GetGlobalEventsResponse, GetGlobalEventStatsData, GetEmailStatsData, ValidateEmailData, ValidateEmailResponse2 as ValidateEmailResponse, TrackClickData, TrackOpenData, GetEmailData, GetEmailTrackingData, GetEmailEventsData, GetEmailLinksData, ListServicesData, ListServicesResponse, CreateServiceData, CreateServiceResponse, ListAvailableContainersData, GetServiceBySlugData, ListServiceHealthStatusesData, ImportExternalServiceData, ImportExternalServiceResponse, ListProjectServicesData, ListProjectServicesResponse, GetProjectServiceEnvironmentVariablesData, GetProvidersMetadataData, GetProviderMetadataData, GetServiceTypesData, GetServiceTypeParametersData, DeleteServiceData, DeleteServiceResponse, GetServiceData, UpdateServiceData, UpdateServiceResponse, GetClusterHealthData, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponse, GetServiceHealthStatusData, AddClusterMemberData, AddClusterMemberResponse, RemoveClusterMemberData, RemoveClusterMemberResponse, GetClusterMemberData, PromoteClusterMemberData, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariableNamesData, ListServiceProjectsData, ListServiceProjectsResponse, LinkServiceToProjectData, LinkServiceToProjectResponse, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponse, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariableData, UpdateServiceResourcesData, UpdateServiceResourcesResponse, StartRestoreData, StartRestoreError, StartRestoreResponse, GetRestoreCapabilitiesData, PlanRestoreData, PlanRestoreError, PlanRestoreResponse, ListRestoreRunsForServiceData, RetryClusterData, RetryClusterResponse, GetServiceRuntimeData, StartServiceData, StartServiceResponse, GetServiceStatsData, StopServiceData, StopServiceResponse, UpgradeServiceData, UpgradeServiceResponse, GetPostgresWalHealthData, ListRootContainersData, ListContainersAtPathData, ListEntitiesData, GetEntityInfoData, QueryDataData, QueryDataResponse2 as QueryDataResponse, DownloadObjectData, GetContainerInfoData, CheckExplorerSupportData, ListPgUpgradesData, StartPgUpgradeData, StartPgUpgradeResponse, GetPgUpgradeData, CancelPgUpgradeData, CancelPgUpgradeResponse, GetPgUpgradeLogsData, RetryPgUpgradeData, RetryPgUpgradeResponse, RollbackPgUpgradeData, RollbackPgUpgradeResponse, GetFileData, GetIpGeolocationData, ListConnectionsData, ListConnectionsResponse, DeleteConnectionData, DeleteConnectionResponse, ActivateConnectionData, DeactivateConnectionData, RunConnectionHealthCheckData, RunConnectionHealthCheckResponse, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponse, SyncRepositoriesData, SyncRepositoriesResponse, UpdateConnectionTokenData, UpdateConnectionTokenResponse, ValidateConnectionData, ListGitProvidersData, CreateGitProviderData, CreateGitProviderResponse, CreateGithubPatProviderData, CreateGithubPatProviderResponse, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponse, CreateGitlabPatProviderData, CreateGitlabPatProviderResponse, DeleteGitProviderData, DeleteGitProviderResponse, GetGitProviderData, ActivateProviderData, HandleGitProviderOauthCallbackData, GetProviderConnectionsData, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponse, DeactivateProviderData, CheckProviderDeletionSafetyData, StartGitProviderOauthData, DeleteProviderSafelyData, DeleteProviderSafelyResponse, GetPublicRepositoryData, GetPublicBranchesData, DetectPublicPresetsData, DiscoverWorkloadsData, DiscoverWorkloadsResponse, ExecuteImportData, ExecuteImportResponse2 as ExecuteImportResponse, CreatePlanData, CreatePlanResponse2 as CreatePlanResponse, ListSourcesData, GetImportStatusData, GetIncidentData, UpdateIncidentStatusData, UpdateIncidentStatusResponse, GetIncidentUpdatesData, AdminListNodesData, RegisterNodeData, RegisterNodeResponse2 as RegisterNodeResponse, AdminRemoveNodeData, AdminRemoveNodeResponse, AdminGetNodeData, AdminListNodeContainersData, PostDnsAckData, PostDnsAckResponse, GetDnsChangesData, AdminUndrainNodeData, AdminUndrainNodeResponse, AdminDrainStatusData, AdminDrainNodeData, AdminDrainNodeResponse, NodeHeartbeatData, NodeHeartbeatResponse, ListPeersData, GetS3CredentialsData, ListIpAccessControlData, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlResponse, CheckIpBlockedData, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlResponse, GetIpAccessControlData, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlResponse, KvDelData, KvDelResponse, KvDisableData, KvDisableResponse, KvEnableData, KvEnableResponse, KvExpireData, KvExpireResponse, KvGetData, KvGetResponse, KvIncrData, KvIncrResponse, KvKeysData, KvKeysResponse, KvSetData, KvSetResponse, KvStatusData, KvTtlData, KvTtlResponse, KvUpdateData, KvUpdateResponse, ListRoutesData, CreateRouteData, CreateRouteResponse, DeleteRouteData, DeleteRouteResponse, GetRouteData, UpdateRouteData, UpdateRouteResponse, LogoutData, GetLogContextData, SearchLogsData, SearchLogsError, SearchLogsResponse2 as SearchLogsResponse, TailLogsData, GetProjectsMonitorHealthData, DeleteMonitorData, DeleteMonitorResponse, GetMonitorData, GetBucketedStatusData, GetCurrentMonitorStatusData, GetUptimeHistoryData, DeletePreferencesData, DeletePreferencesResponse, GetPreferencesData, UpdatePreferencesData, UpdatePreferencesResponse, ListNotificationProvidersData, ListNotificationProvidersResponse, CreateNotificationProviderData, CreateNotificationProviderResponse, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponse, UpdateEmailProviderData, UpdateEmailProviderResponse, CreateSlackProviderData, CreateSlackProviderResponse, UpdateSlackProviderData, UpdateSlackProviderResponse, CreateWebhookProviderData, CreateWebhookProviderResponse, UpdateWebhookProviderData, UpdateWebhookProviderResponse, DeleteNotificationProviderData, DeleteNotificationProviderResponse, GetNotificationProviderData, UpdateNotificationProviderData, UpdateNotificationProviderResponse, TestNotificationProviderData, TestNotificationProviderResponse, ListOrdersData, ListOrdersResponse2 as ListOrdersResponse, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesResponse, GetGenaiTraceData, GetHealthData, ListInsightsData, ListInsightsError, ListInsightsResponse, QueryLogsData, QueryLogsError, QueryLogsResponse, ListMetricNamesData, QueryMetricsData, GetPipelineStatsData, GetQuotaData, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesResponse, QueryTracesData, QueryTracesError, QueryTracesResponse, GetTraceData, IngestLogsData, IngestLogsError, IngestMetricsData, IngestMetricsError, IngestTracesData, IngestTracesError, IngestLogsByPathData, IngestLogsByPathError, IngestMetricsByPathData, IngestMetricsByPathError, IngestTracesByPathData, IngestTracesByPathError, HasPerformanceMetricsData, GetPerformanceMetricsData, GetMetricsOverTimeData, GetGroupedPageMetricsData, GetAccessInfoData, GetPrivateIpData, GetPublicIpData, ListPresetsData, GeneratePresetDockerfileData, GeneratePresetDockerfileResponse, GetPreviewGatewayLogsData, RestartPreviewGatewayData, RestartPreviewGatewayResponse, GetPreviewGatewaySettingsData, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponse, GetPreviewGatewayStatusData, UpgradePreviewGatewayData, UpgradePreviewGatewayResponse, GetProjectsData, GetProjectsResponse, CreateProjectData, CreateProjectResponse, GetProjectBySlugData, CreateProjectFromTemplateData, CreateProjectFromTemplateResponse2 as CreateProjectFromTemplateResponse, GetProjectStatisticsData, DeleteProjectData, DeleteProjectResponse, GetProjectData, UpdateProjectData, UpdateProjectResponse, GetProjectDeploymentsData, GetProjectDeploymentsResponse, GetLastDeploymentData, TriggerProjectPipelineData, TriggerProjectPipelineResponse, GetActiveVisitorsData, ListAgentsData, CreateAgentData, CreateAgentResponse, GetCliStatusData, ListAllRunsData, ListAllRunsResponse, LatestRunForSourceData, GetRunWithLogsData, CancelRunData, CancelRunResponse, RetryRunData, RetryRunResponse, GetSandboxStatusData, SmokeTestAgentData, SmokeTestAgentResponse, DeleteAgentData, DeleteAgentResponse, GetAgentData, UpdateAgentData, UpdateAgentResponse, ListAgentRunsData, ListAgentRunsResponse, TriggerAgentData, TriggerAgentResponse, GetAggregatedBucketsData, StartAnalysisData, StartAnalysisResponse, GetRunData, AddContextData, CancelData, CreatePrData, CreatePrResponse2 as CreatePrResponse, StartFixData, ReAnalyzeData, UpdateAutomaticDeployData, UpdateAutomaticDeployResponse, ListCustomDomainsForProjectData, CreateCustomDomainData, CreateCustomDomainResponse, DeleteCustomDomainData, DeleteCustomDomainResponse, GetCustomDomainData, UpdateCustomDomainData, UpdateCustomDomainResponse, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponse, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponse, GetDeploymentData, CancelDeploymentData, CancelDeploymentResponse, GetDeploymentJobsData, GetDeploymentJobLogsData, TailDeploymentJobLogsData, GetDeploymentOperationsData, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponse, GetDeploymentOperationStatusData, PauseDeploymentData, PauseDeploymentResponse, PromoteDeploymentData, PromoteDeploymentResponse, ResumeDeploymentData, ResumeDeploymentResponse, RollbackToDeploymentData, RollbackToDeploymentResponse, TeardownDeploymentData, TeardownDeploymentResponse, ListDsnsData, CreateDsnData, CreateDsnResponse, GetOrCreateDsnData, GetOrCreateDsnResponse, RegenerateDsnData, RegenerateDsnResponse, RevokeDsnData, RevokeDsnResponse, GetEnvironmentVariablesData, CreateEnvironmentVariableData, CreateEnvironmentVariableResponse, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariableValueData, GetEnvironmentVariableValueData, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponse, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponse, GetEnvironmentsData, CreateEnvironmentData, CreateEnvironmentResponse, DeleteEnvironmentData, DeleteEnvironmentResponse, GetEnvironmentData, GetEnvironmentCronsData, GetCronByIdData, GetCronExecutionsData, GetCronExecutionsResponse, GetEnvironmentDomainsData, AddEnvironmentDomainData, AddEnvironmentDomainResponse, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponse, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponse, SleepEnvironmentData, SleepEnvironmentResponse, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponse, TeardownEnvironmentData, TeardownEnvironmentResponse, WakeEnvironmentData, WakeEnvironmentResponse, GetContainerLogsData, ListContainersData, GetContainerDetailData, GetContainerLogsByIdData, GetContainerMetricsData, StreamContainerMetricsData, RestartContainerData, RestartContainerResponse, StartContainerData, StartContainerResponse, StopContainerData, StopContainerResponse, DeployFromImageData, DeployFromImageResponse, DeployFromImageUploadData, DeployFromImageUploadResponse, DeployFromStaticData, DeployFromStaticResponse, ListAlertRulesData, CreateAlertRuleData, CreateAlertRuleResponse, DeleteAlertRuleData, DeleteAlertRuleResponse, GetAlertRuleData, UpdateAlertRuleData, UpdateAlertRuleResponse, GetErrorDashboardStatsData, ListErrorGroupsData, ListErrorGroupsResponse, GetErrorGroupData, UpdateErrorGroupData, ListErrorEventsData, ListErrorEventsResponse, GetErrorEventData, GetErrorStatsData, GetErrorTimeSeriesData, GetEventsCountData, GetEventTypeBreakdownData, RecordConsoleEventData, GetPropertyBreakdownData, GetPropertyTimelineData, GetEventsTimelineData, GetUniqueEventsData, GetUniqueEventsResponse, ListRemoteExternalImagesData, ListRemoteExternalImagesResponse, RegisterExternalImageData, RegisterExternalImageResponse, DeleteExternalImageData, DeleteExternalImageResponse, GetRemoteExternalImageData, ListFunnelsData, CreateFunnelData, CreateFunnelResponse2 as CreateFunnelResponse, PreviewFunnelMetricsData, PreviewFunnelMetricsResponse, DeleteFunnelData, UpdateFunnelData, GetFunnelMetricsData, UpdateGitSettingsData, UpdateGitSettingsResponse, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponse, HasErrorGroupsData, HasAnalyticsEventsData, GetHourlyVisitsData, ListExternalImagesData, PushExternalImageData, PushExternalImageResponse, GetExternalImageData, ListIncidentsData, CreateIncidentData, CreateIncidentResponse, GetBucketedIncidentsData, PurgeProjectLogsData, PurgeProjectLogsError, ListMcpsData, CreateMcpData, CreateMcpResponse, DeleteMcpData, DeleteMcpResponse, GetMcpData, UpdateMcpData, UpdateMcpResponse, ListMonitorsData, CreateMonitorData, CreateMonitorResponse, ObservabilityListEventsData, ObservabilityFullEventData, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponse, ListSourceMapsData, UploadSourceMapData, UploadSourceMapResponse, RevenueRecentEventsData, RevenueListIntegrationsData, RevenueCreateIntegrationData, RevenueCreateIntegrationResponse, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueUpdateConfigData, RevenueUpdateConfigResponse, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponse, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponse, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueUpdateSecretData, RevenueUpdateSecretResponse, RevenueMetricsCustomersData, RevenueMetricsMrrData, RevenueMetricsSummaryData, ListProjectSecretsData, CreateProjectSecretData, CreateProjectSecretResponse, DeleteProjectSecretData, DeleteProjectSecretResponse, UpdateProjectSecretData, UpdateProjectSecretResponse, UpdateProjectSettingsData, UpdateProjectSettingsResponse, ListSkillsData, CreateSkillData, CreateSkillResponse, UploadSkillData, UploadSkillResponse, DeleteSkillData, DeleteSkillResponse, GetSkillData, UpdateSkillData, UpdateSkillResponse, DownloadSkillArchiveData, ListReleasesData, DeleteSourceMapData, DeleteSourceMapResponse, ListStaticBundlesData, ListStaticBundlesResponse, DeleteStaticBundleData, DeleteStaticBundleResponse, GetStaticBundleData, GetStatusOverviewData, GetUniqueCountsData, UploadStaticBundleData, UploadStaticBundleResponse, ListProjectScansData, ListProjectScansError, ListProjectScansResponse, TriggerScanData, TriggerScanError, TriggerScanResponse2 as TriggerScanResponse, GetLatestScansPerEnvironmentData, GetLatestScanData, ListWebhooksData, ListWebhooksResponse, CreateWebhookData, CreateWebhookResponse, DeleteWebhookData, DeleteWebhookResponse, GetWebhookData, UpdateWebhookData, UpdateWebhookResponse, ListDeliveriesData, GetDeliveryData, RetryDeliveryData, RetryDeliveryResponse, WorkflowDryRunData, WorkflowDryRunResponse, GetProxyLogsData, GetProxyLogsResponse, GetProxyLogByRequestIdData, GetProjectsHealthData, GetTimeBucketStatsData, GetTodayStatsData, GetProxyLogByIdData, ListSyncedRepositoriesData, ListSyncedRepositoriesResponse, GetRepositoryByNameData, GetAllRepositoriesByNameData, GetRepositoryPresetByNameData, GetRepositoryBranchesData, GetRepositoryTagsData, GetRepositoryPresetLiveData, GetRepositoryByIdData, GetBranchesByRepositoryIdData, ListCommitsByRepositoryIdData, CheckCommitExistsData, GetTagsByRepositoryIdData, GetRestoreRunData, RevenueGlobalEventsData, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalSummaryData, RevenueListProvidersData, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysResponse2 as GetProjectSessionReplaysResponse, GetSessionEventsData, GetSettingsData, UpdateSettingsData, UpdateSettingsResponse, SaveAgentTokenData, SaveAgentTokenResponse2 as SaveAgentTokenResponse, ListAiProvidersData, UpdateAiProviderData, UpdateAiProviderResponse2 as UpdateAiProviderResponse, ActivateAiProviderData, ActivateAiProviderResponse, SaveAiProviderCredentialData, SaveAiProviderCredentialResponse, RevokeJoinTokenData, RevokeJoinTokenResponse, GenerateJoinTokenData, GenerateJoinTokenResponse2 as GenerateJoinTokenResponse, GetJoinTokenStatusData, ListGlobalMcpsData, CreateGlobalMcpData, CreateGlobalMcpResponse, DeleteGlobalMcpData, DeleteGlobalMcpResponse, GetGlobalMcpData, UpdateGlobalMcpData, UpdateGlobalMcpResponse, RefreshRouteTableData, RefreshRouteTableResponse, RebuildSandboxImageData, GetGlobalSandboxStatusData, ListSecretsData, UpsertSecretData, UpsertSecretResponse, DeleteSecretData, DeleteSecretResponse, ListGlobalSkillsData, CreateGlobalSkillData, CreateGlobalSkillResponse, UploadGlobalSkillData, UploadGlobalSkillResponse, DeleteGlobalSkillData, DeleteGlobalSkillResponse, GetGlobalSkillData, UpdateGlobalSkillData, UpdateGlobalSkillResponse, DownloadGlobalSkillArchiveData, ListProjectTemplatesData, ListProjectTemplateTagsData, GetProjectTemplateData, GetCurrentUserData, ListUsersData, CreateUserData, CreateUserResponse, UpdateSelfData, UpdateSelfResponse, DisableMfaData, DisableMfaResponse, SetupMfaData, SetupMfaResponse, VerifyAndEnableMfaData, VerifyAndEnableMfaResponse, ChangePasswordSelfData, ChangePasswordSelfResponse, DeleteUserData, DeleteUserResponse, UpdateUserData, UpdateUserResponse, RestoreUserData, RestoreUserResponse, AssignRoleData, RemoveRoleData, RemoveRoleResponse, ListSandboxesData, ListSandboxesResponse2 as ListSandboxesResponse, CreateSandboxData, CreateSandboxResponse, GetSandboxData, CmdData, CmdResponse2 as CmdResponse, GetCmdData, CmdLogsData, DestroySandboxData, DestroySandboxResponse, DomainData, ExecData, ExecResponse2 as ExecResponse, ExecDetachedData, ExecDetachedResponse2 as ExecDetachedResponse, ExtendTimeoutData, ExtendTimeoutResponse, MkdirData, MkdirResponse, ReadFileData, StatPathData, WriteFileData, WriteFileResponse, WriteFilesData, WriteFilesResponse2 as WriteFilesResponse, ListJobsData, JobStatusData, KillJobData, KillJobResponse, JobLogsData, PauseSandboxData, PauseSandboxResponse, ClearPreviewPasswordData, ClearPreviewPasswordResponse, SetPreviewPasswordData, SetPreviewPasswordResponse2 as SetPreviewPasswordResponse, RestartSandboxData, RestartSandboxResponse, ResumeSandboxData, ResumeSandboxResponse, SourceSandboxData, SourceSandboxResponse, StopSandboxData, StopSandboxResponse, CmdKillData, CmdKillResponse, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsResponse2 as GetVisitorSessionsResponse, DeleteSessionReplayData, DeleteSessionReplayError, GetSessionReplayData, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationResponse2 as UpdateSessionDurationResponse, GetSessionReplayEventsData, AddEventsData, AddEventsError, AddEventsResponse2 as AddEventsResponse, DeleteScanData, DeleteScanError, DeleteScanResponse, GetScanData, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesResponse, ListEventTypesData, TriggerWeeklyDigestData, TriggerWeeklyDigestResponse, ListExternalPluginsData, ReloadPluginsData, ReloadPluginsResponse, IngestSentryEnvelopeData, IngestSentryEventData, IngestSentryEventResponse, ListAuditLogsData, ListAuditLogsResponse, GetAuditLogData } from '../types.gen'; import { client } from '../client.gen'; export type QueryKey = [ @@ -1694,6 +1694,27 @@ export const listExternalServiceBackupsInfiniteOptions = (options: Options) => createQueryKey('listServiceSchedules', options); + +/** + * List the schedules that target a specific external service. Useful for + * the service detail page ("which schedules back this DB up?"). + */ +export const listServiceSchedulesOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listServiceSchedules({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listServiceSchedulesQueryKey(options) + }); +}; + export const listS3SourcesQueryKey = (options?: Options) => createQueryKey('listS3Sources', options); /** @@ -2159,6 +2180,64 @@ export const listScheduleRunsInfiniteOptions = (options: Options) => createQueryKey('listScheduleServices', options); + +/** + * List the external services attached to a backup schedule. + */ +export const listScheduleServicesOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listScheduleServices({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listScheduleServicesQueryKey(options) + }); +}; + +/** + * Attach one or more external services to a backup schedule. Idempotent — + * services that are already attached are silently skipped (`ON CONFLICT + * DO NOTHING`). Returns the count of newly inserted rows + the total + * membership after the operation. + */ +export const attachScheduleServicesMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await attachScheduleServices({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Detach a single external service from a backup schedule. Idempotent — + * returns `204` whether or not a row was actually removed. + */ +export const detachScheduleServiceMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await detachScheduleService({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const getBackupQueryKey = (options: Options) => createQueryKey('getBackup', options); /** diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index 7f1c32eb..190e33b4 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type Client, type TDataShape, formDataBodySerializer } from './client'; -import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; +import type { GetPlatformInfoData, GetPlatformInfoResponses, GetPlatformInfoErrors, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CreateReleaseData, CreateReleaseResponses, CreateReleaseErrors, CreateProjectReleaseData, CreateProjectReleaseResponses, CreateProjectReleaseErrors, FinalizeProjectReleaseData, FinalizeProjectReleaseResponses, FinalizeProjectReleaseErrors, ListReleaseFilesData, ListReleaseFilesResponses, ListReleaseFilesErrors, UploadReleaseFileData, UploadReleaseFileResponses, UploadReleaseFileErrors, RecordEventMetricsData, RecordEventMetricsResponses, RecordEventMetricsErrors, AddSessionReplayEventsData, AddSessionReplayEventsResponses, AddSessionReplayEventsErrors, InitSessionReplayData, InitSessionReplayResponses, InitSessionReplayErrors, RecordSpeedMetricsData, RecordSpeedMetricsResponses, RecordSpeedMetricsErrors, UpdateSpeedMetricsData, UpdateSpeedMetricsResponses, UpdateSpeedMetricsErrors, WebhookTriggerData, WebhookTriggerResponses, WebhookTriggerErrors, GetPricingData, GetPricingResponses, GetPricingErrors, ListProviderKeysData, ListProviderKeysResponses, ListProviderKeysErrors, CreateProviderKeyData, CreateProviderKeyResponses, CreateProviderKeyErrors, TestProviderKeyInlineData, TestProviderKeyInlineResponses, TestProviderKeyInlineErrors, DeleteProviderKeyData, DeleteProviderKeyResponses, DeleteProviderKeyErrors, UpdateProviderKeyData, UpdateProviderKeyResponses, UpdateProviderKeyErrors, TestProviderKeyByIdData, TestProviderKeyByIdResponses, TestProviderKeyByIdErrors, GetUsageByProviderData, GetUsageByProviderResponses, GetUsageByProviderErrors, GetConversationsData, GetConversationsResponses, GetConversationsErrors, GetConversationDetailData, GetConversationDetailResponses, GetConversationDetailErrors, GetUsageRecentData, GetUsageRecentResponses, GetUsageRecentErrors, GetUsageSummaryData, GetUsageSummaryResponses, GetUsageSummaryErrors, GetUsageTimeseriesData, GetUsageTimeseriesResponses, GetUsageTimeseriesErrors, GetUsageTopModelsData, GetUsageTopModelsResponses, GetUsageTopModelsErrors, ChatCompletionsData, ChatCompletionsResponses, ChatCompletionsErrors, EmbeddingsData, EmbeddingsResponses, EmbeddingsErrors, ListModelsData, ListModelsResponses, ListModelsErrors, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsResponses, GetAnalyticsActiveVisitorsErrors, GetEventDetailData, GetEventDetailResponses, GetEventDetailErrors, GetEventVisitorsData, GetEventVisitorsResponses, GetEventVisitorsErrors, GetAnalyticsEventsCountData, GetAnalyticsEventsCountResponses, GetAnalyticsEventsCountErrors, GetGeneralStatsData, GetGeneralStatsResponses, GetGeneralStatsErrors, GetLiveVisitorsListData, GetLiveVisitorsListResponses, GetLiveVisitorsListErrors, GetPageFlowData, GetPageFlowResponses, GetPageFlowErrors, GetPageHourlySessionsData, GetPageHourlySessionsResponses, GetPageHourlySessionsErrors, GetPagePathDetailData, GetPagePathDetailResponses, GetPagePathDetailErrors, GetPagePathVisitorsData, GetPagePathVisitorsResponses, GetPagePathVisitorsErrors, GetPagePathsData, GetPagePathsResponses, GetPagePathsErrors, GetPagePathsSparklinesData, GetPagePathsSparklinesResponses, GetPagePathsSparklinesErrors, GetRecentActivityData, GetRecentActivityResponses, GetRecentActivityErrors, GetSessionDetailsData, GetSessionDetailsResponses, GetSessionDetailsErrors, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsResponses, GetAnalyticsSessionEventsErrors, GetSessionLogsData, GetSessionLogsResponses, GetSessionLogsErrors, GetVisitorFacetsData, GetVisitorFacetsResponses, GetVisitorFacetsErrors, GetVisitorsData, GetVisitorsResponses, GetVisitorsErrors, GetVisitorByGuidData, GetVisitorByGuidResponses, GetVisitorByGuidErrors, GetVisitorByIdData, GetVisitorByIdResponses, GetVisitorByIdErrors, GetVisitorDetailsData, GetVisitorDetailsResponses, GetVisitorDetailsErrors, EnrichVisitorData, EnrichVisitorResponses, EnrichVisitorErrors, GetVisitorInfoData, GetVisitorInfoResponses, GetVisitorInfoErrors, GetVisitorJourneyData, GetVisitorJourneyResponses, GetVisitorJourneyErrors, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsResponses, GetAnalyticsVisitorSessionsErrors, GetVisitorStatsData, GetVisitorStatsResponses, GetVisitorStatsErrors, ListApiKeysData, ListApiKeysResponses, ListApiKeysErrors, CreateApiKeyData, CreateApiKeyResponses, CreateApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsResponses, GetApiKeyPermissionsErrors, DeleteApiKeyData, DeleteApiKeyResponses, DeleteApiKeyErrors, GetApiKeyData, GetApiKeyResponses, GetApiKeyErrors, UpdateApiKeyData, UpdateApiKeyResponses, UpdateApiKeyErrors, ActivateApiKeyData, ActivateApiKeyResponses, ActivateApiKeyErrors, DeactivateApiKeyData, DeactivateApiKeyResponses, DeactivateApiKeyErrors, CliDeviceApproveData, CliDeviceApproveResponses, CliDeviceApproveErrors, CliDeviceDenyData, CliDeviceDenyResponses, CliDeviceDenyErrors, CliDeviceLookupData, CliDeviceLookupResponses, CliDeviceLookupErrors, CliDevicePollData, CliDevicePollResponses, CliDevicePollErrors, CliDeviceStartData, CliDeviceStartResponses, CliDeviceStartErrors, CliLogoutData, CliLogoutResponses, CliLogoutErrors, EmailStatusData, EmailStatusResponses, EmailStatusErrors, LoginData, LoginResponses, LoginErrors, RequestMagicLinkData, RequestMagicLinkResponses, RequestMagicLinkErrors, VerifyMagicLinkData, VerifyMagicLinkResponses, VerifyMagicLinkErrors, RequestPasswordResetData, RequestPasswordResetResponses, RequestPasswordResetErrors, ResetPasswordData, ResetPasswordResponses, ResetPasswordErrors, VerifyEmailData, VerifyEmailResponses, VerifyEmailErrors, VerifyMfaChallengeData, VerifyMfaChallengeResponses, VerifyMfaChallengeErrors, ListBackupAlertsData, ListBackupAlertsResponses, ListBackupAlertsErrors, RunExternalServiceBackupData, RunExternalServiceBackupResponses, RunExternalServiceBackupErrors, ListExternalServiceBackupsData, ListExternalServiceBackupsResponses, ListExternalServiceBackupsErrors, ListServiceSchedulesData, ListServiceSchedulesResponses, ListServiceSchedulesErrors, ListS3SourcesData, ListS3SourcesResponses, ListS3SourcesErrors, CreateS3SourceData, CreateS3SourceResponses, CreateS3SourceErrors, TestS3ConnectionPreviewData, TestS3ConnectionPreviewResponses, TestS3ConnectionPreviewErrors, DeleteS3SourceData, DeleteS3SourceResponses, DeleteS3SourceErrors, GetS3SourceData, GetS3SourceResponses, GetS3SourceErrors, UpdateS3SourceData, UpdateS3SourceResponses, UpdateS3SourceErrors, ListSourceBackupsData, ListSourceBackupsResponses, ListSourceBackupsErrors, RunBackupForSourceData, RunBackupForSourceResponses, RunBackupForSourceErrors, SetDefaultS3SourceData, SetDefaultS3SourceResponses, SetDefaultS3SourceErrors, TestS3SourceConnectionData, TestS3SourceConnectionResponses, TestS3SourceConnectionErrors, CancelScheduleRunData, CancelScheduleRunResponses, CancelScheduleRunErrors, ListScheduleRunJobsData, ListScheduleRunJobsResponses, ListScheduleRunJobsErrors, ListBackupSchedulesData, ListBackupSchedulesResponses, ListBackupSchedulesErrors, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateBackupScheduleErrors, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteBackupScheduleErrors, GetBackupScheduleData, GetBackupScheduleResponses, GetBackupScheduleErrors, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateBackupScheduleErrors, ListBackupsForScheduleData, ListBackupsForScheduleResponses, ListBackupsForScheduleErrors, DisableBackupScheduleData, DisableBackupScheduleResponses, DisableBackupScheduleErrors, EnableBackupScheduleData, EnableBackupScheduleResponses, EnableBackupScheduleErrors, RunScheduleNowData, RunScheduleNowResponses, RunScheduleNowErrors, ListScheduleRunsData, ListScheduleRunsResponses, ListScheduleRunsErrors, ListScheduleServicesData, ListScheduleServicesResponses, ListScheduleServicesErrors, AttachScheduleServicesData, AttachScheduleServicesResponses, AttachScheduleServicesErrors, DetachScheduleServiceData, DetachScheduleServiceResponses, DetachScheduleServiceErrors, GetBackupData, GetBackupResponses, GetBackupErrors, CancelBackupData, CancelBackupResponses, CancelBackupErrors, ListBackupChildrenData, ListBackupChildrenResponses, ListBackupChildrenErrors, BlobDeleteData, BlobDeleteResponses, BlobDeleteErrors, BlobListData, BlobListResponses, BlobListErrors, BlobPutData, BlobPutResponses, BlobPutErrors, BlobCopyData, BlobCopyResponses, BlobCopyErrors, BlobDisableData, BlobDisableResponses, BlobDisableErrors, BlobEnableData, BlobEnableResponses, BlobEnableErrors, BlobStatusData, BlobStatusResponses, BlobStatusErrors, BlobUpdateData, BlobUpdateResponses, BlobUpdateErrors, BlobDownloadData, BlobDownloadResponses, BlobDownloadErrors, BlobHeadData, BlobHeadResponses, BlobHeadErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsResponses, GetDashboardProjectsAnalyticsErrors, GetActivityGraphData, GetActivityGraphResponses, GetActivityGraphErrors, GetScanByDeploymentData, GetScanByDeploymentResponses, GetScanByDeploymentErrors, ListDnsProvidersData, ListDnsProvidersResponses, ListDnsProvidersErrors, CreateDnsProviderData, CreateDnsProviderResponses, CreateDnsProviderErrors, DeleteDnsProviderData, DeleteDnsProviderResponses, DeleteDnsProviderErrors, GetDnsProviderData, GetDnsProviderResponses, GetDnsProviderErrors, UpdateProviderData, UpdateProviderResponses, UpdateProviderErrors, ListManagedDomainsData, ListManagedDomainsResponses, ListManagedDomainsErrors, AddManagedDomainData, AddManagedDomainResponses, AddManagedDomainErrors, TestProviderConnectionData, TestProviderConnectionResponses, TestProviderConnectionErrors, ListProviderZonesData, ListProviderZonesResponses, ListProviderZonesErrors, RemoveManagedDomainData, RemoveManagedDomainResponses, RemoveManagedDomainErrors, VerifyManagedDomainData, VerifyManagedDomainResponses, VerifyManagedDomainErrors, LookupDnsARecordsData, LookupDnsARecordsResponses, LookupDnsARecordsErrors, ListDomainsData, ListDomainsResponses, ListDomainsErrors, CreateDomainData, CreateDomainResponses, CreateDomainErrors, GetDomainByHostData, GetDomainByHostResponses, GetDomainByHostErrors, CancelDomainOrderData, CancelDomainOrderResponses, CancelDomainOrderErrors, GetDomainOrderData, GetDomainOrderResponses, GetDomainOrderErrors, CreateOrRecreateOrderData, CreateOrRecreateOrderResponses, CreateOrRecreateOrderErrors, FinalizeOrderData, FinalizeOrderResponses, FinalizeOrderErrors, SetupDnsChallengeData, SetupDnsChallengeResponses, SetupDnsChallengeErrors, DeleteDomainData, DeleteDomainResponses, DeleteDomainErrors, GetDomainByIdData, GetDomainByIdResponses, GetDomainByIdErrors, GetChallengeTokenData, GetChallengeTokenResponses, GetChallengeTokenErrors, GetHttpChallengeDebugData, GetHttpChallengeDebugResponses, GetHttpChallengeDebugErrors, ProvisionDomainData, ProvisionDomainResponses, ProvisionDomainErrors, RenewDomainData, RenewDomainResponses, RenewDomainErrors, CheckDomainStatusData, CheckDomainStatusResponses, CheckDomainStatusErrors, ListEmailDomainsData, ListEmailDomainsResponses, ListEmailDomainsErrors, CreateEmailDomainData, CreateEmailDomainResponses, CreateEmailDomainErrors, GetDomainByNameData, GetDomainByNameResponses, GetDomainByNameErrors, DeleteEmailDomainData, DeleteEmailDomainResponses, DeleteEmailDomainErrors, GetDomainData, GetDomainResponses, GetDomainErrors, GetDomainDnsRecordsData, GetDomainDnsRecordsResponses, GetDomainDnsRecordsErrors, SetupDnsData, SetupDnsResponses, SetupDnsErrors, VerifyDomainData, VerifyDomainResponses, VerifyDomainErrors, ListEmailProvidersData, ListEmailProvidersResponses, ListEmailProvidersErrors, CreateEmailProviderData, CreateEmailProviderResponses, CreateEmailProviderErrors, DeleteEmailProviderData, DeleteEmailProviderResponses, DeleteEmailProviderErrors, GetEmailProviderData, GetEmailProviderResponses, GetEmailProviderErrors, TestProviderData, TestProviderResponses, TestProviderErrors, ListEmailsData, ListEmailsResponses, ListEmailsErrors, SendEmailData, SendEmailResponses, SendEmailErrors, GetGlobalEventsData, GetGlobalEventsResponses, GetGlobalEventsErrors, GetGlobalEventStatsData, GetGlobalEventStatsResponses, GetGlobalEventStatsErrors, GetEmailStatsData, GetEmailStatsResponses, GetEmailStatsErrors, ValidateEmailData, ValidateEmailResponses, ValidateEmailErrors, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenResponses, TrackOpenErrors, GetEmailData, GetEmailResponses, GetEmailErrors, GetEmailTrackingData, GetEmailTrackingResponses, GetEmailTrackingErrors, GetEmailEventsData, GetEmailEventsResponses, GetEmailEventsErrors, GetEmailLinksData, GetEmailLinksResponses, GetEmailLinksErrors, ListServicesData, ListServicesResponses, ListServicesErrors, CreateServiceData, CreateServiceResponses, CreateServiceErrors, ListAvailableContainersData, ListAvailableContainersResponses, ListAvailableContainersErrors, GetServiceBySlugData, GetServiceBySlugResponses, GetServiceBySlugErrors, ListServiceHealthStatusesData, ListServiceHealthStatusesResponses, ListServiceHealthStatusesErrors, ImportExternalServiceData, ImportExternalServiceResponses, ImportExternalServiceErrors, ListProjectServicesData, ListProjectServicesResponses, ListProjectServicesErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceEnvironmentVariablesErrors, GetProvidersMetadataData, GetProvidersMetadataResponses, GetProvidersMetadataErrors, GetProviderMetadataData, GetProviderMetadataResponses, GetProviderMetadataErrors, GetServiceTypesData, GetServiceTypesResponses, GetServiceTypesErrors, GetServiceTypeParametersData, GetServiceTypeParametersResponses, GetServiceTypeParametersErrors, DeleteServiceData, DeleteServiceResponses, DeleteServiceErrors, GetServiceData, GetServiceResponses, GetServiceErrors, UpdateServiceData, UpdateServiceResponses, UpdateServiceErrors, GetClusterHealthData, GetClusterHealthResponses, GetClusterHealthErrors, TriggerServiceHealthCheckData, TriggerServiceHealthCheckResponses, TriggerServiceHealthCheckErrors, GetServiceHealthStatusData, GetServiceHealthStatusResponses, GetServiceHealthStatusErrors, AddClusterMemberData, AddClusterMemberResponses, AddClusterMemberErrors, RemoveClusterMemberData, RemoveClusterMemberResponses, RemoveClusterMemberErrors, GetClusterMemberData, GetClusterMemberResponses, GetClusterMemberErrors, PromoteClusterMemberData, PromoteClusterMemberResponses, PromoteClusterMemberErrors, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariableNamesErrors, ListServiceProjectsData, ListServiceProjectsResponses, ListServiceProjectsErrors, LinkServiceToProjectData, LinkServiceToProjectResponses, LinkServiceToProjectErrors, UnlinkServiceFromProjectData, UnlinkServiceFromProjectResponses, UnlinkServiceFromProjectErrors, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesResponses, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariableErrors, UpdateServiceResourcesData, UpdateServiceResourcesResponses, UpdateServiceResourcesErrors, StartRestoreData, StartRestoreResponses, StartRestoreErrors, GetRestoreCapabilitiesData, GetRestoreCapabilitiesResponses, GetRestoreCapabilitiesErrors, PlanRestoreData, PlanRestoreResponses, PlanRestoreErrors, ListRestoreRunsForServiceData, ListRestoreRunsForServiceResponses, RetryClusterData, RetryClusterResponses, RetryClusterErrors, GetServiceRuntimeData, GetServiceRuntimeResponses, GetServiceRuntimeErrors, StartServiceData, StartServiceResponses, StartServiceErrors, GetServiceStatsData, GetServiceStatsResponses, GetServiceStatsErrors, StopServiceData, StopServiceResponses, StopServiceErrors, UpgradeServiceData, UpgradeServiceResponses, UpgradeServiceErrors, GetPostgresWalHealthData, GetPostgresWalHealthResponses, GetPostgresWalHealthErrors, ListRootContainersData, ListRootContainersResponses, ListRootContainersErrors, ListContainersAtPathData, ListContainersAtPathResponses, ListContainersAtPathErrors, ListEntitiesData, ListEntitiesResponses, ListEntitiesErrors, GetEntityInfoData, GetEntityInfoResponses, GetEntityInfoErrors, QueryDataData, QueryDataResponses, QueryDataErrors, DownloadObjectData, DownloadObjectResponses, DownloadObjectErrors, GetContainerInfoData, GetContainerInfoResponses, GetContainerInfoErrors, CheckExplorerSupportData, CheckExplorerSupportResponses, CheckExplorerSupportErrors, ListPgUpgradesData, ListPgUpgradesResponses, ListPgUpgradesErrors, StartPgUpgradeData, StartPgUpgradeResponses, StartPgUpgradeErrors, GetPgUpgradeData, GetPgUpgradeResponses, GetPgUpgradeErrors, CancelPgUpgradeData, CancelPgUpgradeResponses, CancelPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsResponses, GetPgUpgradeLogsErrors, RetryPgUpgradeData, RetryPgUpgradeResponses, RetryPgUpgradeErrors, RollbackPgUpgradeData, RollbackPgUpgradeResponses, RollbackPgUpgradeErrors, GetFileData, GetFileResponses, GetFileErrors, GetIpGeolocationData, GetIpGeolocationResponses, GetIpGeolocationErrors, ListConnectionsData, ListConnectionsResponses, ListConnectionsErrors, DeleteConnectionData, DeleteConnectionResponses, DeleteConnectionErrors, ActivateConnectionData, ActivateConnectionResponses, ActivateConnectionErrors, DeactivateConnectionData, DeactivateConnectionResponses, DeactivateConnectionErrors, RunConnectionHealthCheckData, RunConnectionHealthCheckResponses, RunConnectionHealthCheckErrors, ListRepositoriesByConnectionData, ListRepositoriesByConnectionResponses, ListRepositoriesByConnectionErrors, SyncRepositoriesData, SyncRepositoriesResponses, SyncRepositoriesErrors, UpdateConnectionTokenData, UpdateConnectionTokenResponses, UpdateConnectionTokenErrors, ValidateConnectionData, ValidateConnectionResponses, ValidateConnectionErrors, ListGitProvidersData, ListGitProvidersResponses, ListGitProvidersErrors, CreateGitProviderData, CreateGitProviderResponses, CreateGitProviderErrors, CreateGithubPatProviderData, CreateGithubPatProviderResponses, CreateGithubPatProviderErrors, CreateGitlabOauthProviderData, CreateGitlabOauthProviderResponses, CreateGitlabOauthProviderErrors, CreateGitlabPatProviderData, CreateGitlabPatProviderResponses, CreateGitlabPatProviderErrors, DeleteGitProviderData, DeleteGitProviderResponses, DeleteGitProviderErrors, GetGitProviderData, GetGitProviderResponses, GetGitProviderErrors, ActivateProviderData, ActivateProviderResponses, ActivateProviderErrors, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, GetProviderConnectionsData, GetProviderConnectionsResponses, GetProviderConnectionsErrors, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsResponses, UpdateGitProviderCredentialsErrors, DeactivateProviderData, DeactivateProviderResponses, DeactivateProviderErrors, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyResponses, CheckProviderDeletionSafetyErrors, StartGitProviderOauthData, StartGitProviderOauthErrors, DeleteProviderSafelyData, DeleteProviderSafelyResponses, DeleteProviderSafelyErrors, GetPublicRepositoryData, GetPublicRepositoryResponses, GetPublicRepositoryErrors, GetPublicBranchesData, GetPublicBranchesResponses, GetPublicBranchesErrors, DetectPublicPresetsData, DetectPublicPresetsResponses, DetectPublicPresetsErrors, DiscoverWorkloadsData, DiscoverWorkloadsResponses, DiscoverWorkloadsErrors, ExecuteImportData, ExecuteImportResponses, ExecuteImportErrors, CreatePlanData, CreatePlanResponses, CreatePlanErrors, ListSourcesData, ListSourcesResponses, ListSourcesErrors, GetImportStatusData, GetImportStatusResponses, GetImportStatusErrors, GetIncidentData, GetIncidentResponses, GetIncidentErrors, UpdateIncidentStatusData, UpdateIncidentStatusResponses, UpdateIncidentStatusErrors, GetIncidentUpdatesData, GetIncidentUpdatesResponses, GetIncidentUpdatesErrors, AdminListNodesData, AdminListNodesResponses, AdminListNodesErrors, RegisterNodeData, RegisterNodeResponses, RegisterNodeErrors, AdminRemoveNodeData, AdminRemoveNodeResponses, AdminRemoveNodeErrors, AdminGetNodeData, AdminGetNodeResponses, AdminGetNodeErrors, AdminListNodeContainersData, AdminListNodeContainersResponses, AdminListNodeContainersErrors, PostDnsAckData, PostDnsAckResponses, PostDnsAckErrors, GetDnsChangesData, GetDnsChangesResponses, GetDnsChangesErrors, AdminUndrainNodeData, AdminUndrainNodeResponses, AdminUndrainNodeErrors, AdminDrainStatusData, AdminDrainStatusResponses, AdminDrainStatusErrors, AdminDrainNodeData, AdminDrainNodeResponses, AdminDrainNodeErrors, NodeHeartbeatData, NodeHeartbeatResponses, NodeHeartbeatErrors, ListPeersData, ListPeersResponses, ListPeersErrors, GetS3CredentialsData, GetS3CredentialsResponses, GetS3CredentialsErrors, ListIpAccessControlData, ListIpAccessControlResponses, ListIpAccessControlErrors, CreateIpAccessControlData, CreateIpAccessControlResponses, CreateIpAccessControlErrors, CheckIpBlockedData, CheckIpBlockedResponses, CheckIpBlockedErrors, DeleteIpAccessControlData, DeleteIpAccessControlResponses, DeleteIpAccessControlErrors, GetIpAccessControlData, GetIpAccessControlResponses, GetIpAccessControlErrors, UpdateIpAccessControlData, UpdateIpAccessControlResponses, UpdateIpAccessControlErrors, KvDelData, KvDelResponses, KvDelErrors, KvDisableData, KvDisableResponses, KvDisableErrors, KvEnableData, KvEnableResponses, KvEnableErrors, KvExpireData, KvExpireResponses, KvExpireErrors, KvGetData, KvGetResponses, KvGetErrors, KvIncrData, KvIncrResponses, KvIncrErrors, KvKeysData, KvKeysResponses, KvKeysErrors, KvSetData, KvSetResponses, KvSetErrors, KvStatusData, KvStatusResponses, KvStatusErrors, KvTtlData, KvTtlResponses, KvTtlErrors, KvUpdateData, KvUpdateResponses, KvUpdateErrors, ListRoutesData, ListRoutesResponses, ListRoutesErrors, CreateRouteData, CreateRouteResponses, CreateRouteErrors, DeleteRouteData, DeleteRouteResponses, DeleteRouteErrors, GetRouteData, GetRouteResponses, GetRouteErrors, UpdateRouteData, UpdateRouteResponses, UpdateRouteErrors, LogoutData, LogoutResponses, LogoutErrors, GetLogContextData, GetLogContextResponses, GetLogContextErrors, SearchLogsData, SearchLogsResponses, SearchLogsErrors, TailLogsData, TailLogsResponses, TailLogsErrors, GetProjectsMonitorHealthData, GetProjectsMonitorHealthResponses, GetProjectsMonitorHealthErrors, DeleteMonitorData, DeleteMonitorResponses, DeleteMonitorErrors, GetMonitorData, GetMonitorResponses, GetMonitorErrors, GetBucketedStatusData, GetBucketedStatusResponses, GetBucketedStatusErrors, GetCurrentMonitorStatusData, GetCurrentMonitorStatusResponses, GetCurrentMonitorStatusErrors, GetUptimeHistoryData, GetUptimeHistoryResponses, GetUptimeHistoryErrors, DeletePreferencesData, DeletePreferencesResponses, DeletePreferencesErrors, GetPreferencesData, GetPreferencesResponses, GetPreferencesErrors, UpdatePreferencesData, UpdatePreferencesResponses, UpdatePreferencesErrors, ListNotificationProvidersData, ListNotificationProvidersResponses, ListNotificationProvidersErrors, CreateNotificationProviderData, CreateNotificationProviderResponses, CreateNotificationProviderErrors, CreateNotificationEmailProviderData, CreateNotificationEmailProviderResponses, CreateNotificationEmailProviderErrors, UpdateEmailProviderData, UpdateEmailProviderResponses, UpdateEmailProviderErrors, CreateSlackProviderData, CreateSlackProviderResponses, CreateSlackProviderErrors, UpdateSlackProviderData, UpdateSlackProviderResponses, UpdateSlackProviderErrors, CreateWebhookProviderData, CreateWebhookProviderResponses, CreateWebhookProviderErrors, UpdateWebhookProviderData, UpdateWebhookProviderResponses, UpdateWebhookProviderErrors, DeleteNotificationProviderData, DeleteNotificationProviderResponses, DeleteNotificationProviderErrors, GetNotificationProviderData, GetNotificationProviderResponses, GetNotificationProviderErrors, UpdateNotificationProviderData, UpdateNotificationProviderResponses, UpdateNotificationProviderErrors, TestNotificationProviderData, TestNotificationProviderResponses, TestNotificationProviderErrors, ListOrdersData, ListOrdersResponses, ListOrdersErrors, QueryGenaiTracesData, QueryGenaiTracesResponses, QueryGenaiTracesErrors, GetGenaiTraceData, GetGenaiTraceResponses, GetGenaiTraceErrors, GetHealthData, GetHealthResponses, GetHealthErrors, ListInsightsData, ListInsightsResponses, ListInsightsErrors, QueryLogsData, QueryLogsResponses, QueryLogsErrors, ListMetricNamesData, ListMetricNamesResponses, ListMetricNamesErrors, QueryMetricsData, QueryMetricsResponses, QueryMetricsErrors, GetPipelineStatsData, GetPipelineStatsResponses, GetPipelineStatsErrors, GetQuotaData, GetQuotaResponses, GetQuotaErrors, QueryTraceSummariesData, QueryTraceSummariesResponses, QueryTraceSummariesErrors, QueryTracesData, QueryTracesResponses, QueryTracesErrors, GetTraceData, GetTraceResponses, GetTraceErrors, IngestLogsData, IngestLogsResponses, IngestLogsErrors, IngestMetricsData, IngestMetricsResponses, IngestMetricsErrors, IngestTracesData, IngestTracesResponses, IngestTracesErrors, IngestLogsByPathData, IngestLogsByPathResponses, IngestLogsByPathErrors, IngestMetricsByPathData, IngestMetricsByPathResponses, IngestMetricsByPathErrors, IngestTracesByPathData, IngestTracesByPathResponses, IngestTracesByPathErrors, HasPerformanceMetricsData, HasPerformanceMetricsResponses, HasPerformanceMetricsErrors, GetPerformanceMetricsData, GetPerformanceMetricsResponses, GetPerformanceMetricsErrors, GetMetricsOverTimeData, GetMetricsOverTimeResponses, GetMetricsOverTimeErrors, GetGroupedPageMetricsData, GetGroupedPageMetricsResponses, GetGroupedPageMetricsErrors, GetAccessInfoData, GetAccessInfoResponses, GetAccessInfoErrors, GetPrivateIpData, GetPrivateIpResponses, GetPrivateIpErrors, GetPublicIpData, GetPublicIpResponses, GetPublicIpErrors, ListPresetsData, ListPresetsResponses, ListPresetsErrors, GeneratePresetDockerfileData, GeneratePresetDockerfileResponses, GeneratePresetDockerfileErrors, GetPreviewGatewayLogsData, GetPreviewGatewayLogsResponses, RestartPreviewGatewayData, RestartPreviewGatewayResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayResponses, GetProjectsData, GetProjectsResponses, GetProjectsErrors, CreateProjectData, CreateProjectResponses, CreateProjectErrors, GetProjectBySlugData, GetProjectBySlugResponses, GetProjectBySlugErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateResponses, CreateProjectFromTemplateErrors, GetProjectStatisticsData, GetProjectStatisticsResponses, GetProjectStatisticsErrors, DeleteProjectData, DeleteProjectResponses, DeleteProjectErrors, GetProjectData, GetProjectResponses, GetProjectErrors, UpdateProjectData, UpdateProjectResponses, UpdateProjectErrors, GetProjectDeploymentsData, GetProjectDeploymentsResponses, GetProjectDeploymentsErrors, GetLastDeploymentData, GetLastDeploymentResponses, GetLastDeploymentErrors, TriggerProjectPipelineData, TriggerProjectPipelineResponses, TriggerProjectPipelineErrors, GetActiveVisitorsData, GetActiveVisitorsResponses, GetActiveVisitorsErrors, ListAgentsData, ListAgentsResponses, ListAgentsErrors, CreateAgentData, CreateAgentResponses, CreateAgentErrors, GetCliStatusData, GetCliStatusResponses, GetCliStatusErrors, ListAllRunsData, ListAllRunsResponses, ListAllRunsErrors, LatestRunForSourceData, LatestRunForSourceResponses, LatestRunForSourceErrors, GetRunWithLogsData, GetRunWithLogsResponses, GetRunWithLogsErrors, CancelRunData, CancelRunResponses, CancelRunErrors, RetryRunData, RetryRunResponses, RetryRunErrors, StreamRunEventsData, StreamRunEventsResponses, StreamRunEventsErrors, GetSandboxStatusData, GetSandboxStatusResponses, GetSandboxStatusErrors, SmokeTestAgentData, SmokeTestAgentResponses, SmokeTestAgentErrors, DeleteAgentData, DeleteAgentResponses, DeleteAgentErrors, GetAgentData, GetAgentResponses, GetAgentErrors, UpdateAgentData, UpdateAgentResponses, UpdateAgentErrors, ListAgentRunsData, ListAgentRunsResponses, ListAgentRunsErrors, TriggerAgentData, TriggerAgentResponses, TriggerAgentErrors, GetAggregatedBucketsData, GetAggregatedBucketsResponses, GetAggregatedBucketsErrors, StartAnalysisData, StartAnalysisResponses, StartAnalysisErrors, GetRunData, GetRunResponses, GetRunErrors, AddContextData, AddContextResponses, AddContextErrors, CancelData, CancelResponses, CancelErrors, CreatePrData, CreatePrResponses, CreatePrErrors, StartFixData, StartFixResponses, StartFixErrors, ReAnalyzeData, ReAnalyzeResponses, ReAnalyzeErrors, StreamEventsData, StreamEventsResponses, StreamEventsErrors, UpdateAutomaticDeployData, UpdateAutomaticDeployResponses, UpdateAutomaticDeployErrors, ListCustomDomainsForProjectData, ListCustomDomainsForProjectResponses, ListCustomDomainsForProjectErrors, CreateCustomDomainData, CreateCustomDomainResponses, CreateCustomDomainErrors, DeleteCustomDomainData, DeleteCustomDomainResponses, DeleteCustomDomainErrors, GetCustomDomainData, GetCustomDomainResponses, GetCustomDomainErrors, UpdateCustomDomainData, UpdateCustomDomainResponses, UpdateCustomDomainErrors, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateResponses, LinkCustomDomainToCertificateErrors, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigResponses, UpdateProjectDeploymentConfigErrors, GetDeploymentData, GetDeploymentResponses, GetDeploymentErrors, CancelDeploymentData, CancelDeploymentResponses, CancelDeploymentErrors, GetDeploymentJobsData, GetDeploymentJobsResponses, GetDeploymentJobsErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsResponses, GetDeploymentJobLogsErrors, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, GetDeploymentOperationsData, GetDeploymentOperationsResponses, GetDeploymentOperationsErrors, ExecuteDeploymentOperationData, ExecuteDeploymentOperationResponses, ExecuteDeploymentOperationErrors, GetDeploymentOperationStatusData, GetDeploymentOperationStatusResponses, GetDeploymentOperationStatusErrors, PauseDeploymentData, PauseDeploymentResponses, PauseDeploymentErrors, PromoteDeploymentData, PromoteDeploymentResponses, PromoteDeploymentErrors, ResumeDeploymentData, ResumeDeploymentResponses, ResumeDeploymentErrors, RollbackToDeploymentData, RollbackToDeploymentResponses, RollbackToDeploymentErrors, TeardownDeploymentData, TeardownDeploymentResponses, TeardownDeploymentErrors, ListDsnsData, ListDsnsResponses, CreateDsnData, CreateDsnResponses, CreateDsnErrors, GetOrCreateDsnData, GetOrCreateDsnResponses, GetOrCreateDsnErrors, RegenerateDsnData, RegenerateDsnResponses, RegenerateDsnErrors, RevokeDsnData, RevokeDsnResponses, RevokeDsnErrors, GetEnvironmentVariablesData, GetEnvironmentVariablesResponses, GetEnvironmentVariablesErrors, CreateEnvironmentVariableData, CreateEnvironmentVariableResponses, CreateEnvironmentVariableErrors, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueResponses, GetResolvedEnvironmentVariableValueErrors, GetEnvironmentVariableValueData, GetEnvironmentVariableValueResponses, GetEnvironmentVariableValueErrors, DeleteEnvironmentVariableData, DeleteEnvironmentVariableResponses, DeleteEnvironmentVariableErrors, UpdateEnvironmentVariableData, UpdateEnvironmentVariableResponses, UpdateEnvironmentVariableErrors, GetEnvironmentsData, GetEnvironmentsResponses, GetEnvironmentsErrors, CreateEnvironmentData, CreateEnvironmentResponses, CreateEnvironmentErrors, DeleteEnvironmentData, DeleteEnvironmentResponses, DeleteEnvironmentErrors, GetEnvironmentData, GetEnvironmentResponses, GetEnvironmentErrors, GetEnvironmentCronsData, GetEnvironmentCronsResponses, GetEnvironmentCronsErrors, GetCronByIdData, GetCronByIdResponses, GetCronByIdErrors, GetCronExecutionsData, GetCronExecutionsResponses, GetCronExecutionsErrors, GetEnvironmentDomainsData, GetEnvironmentDomainsResponses, GetEnvironmentDomainsErrors, AddEnvironmentDomainData, AddEnvironmentDomainResponses, AddEnvironmentDomainErrors, DeleteEnvironmentDomainData, DeleteEnvironmentDomainResponses, DeleteEnvironmentDomainErrors, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSettingsErrors, SleepEnvironmentData, SleepEnvironmentResponses, SleepEnvironmentErrors, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentSubdomainErrors, TeardownEnvironmentData, TeardownEnvironmentResponses, TeardownEnvironmentErrors, WakeEnvironmentData, WakeEnvironmentResponses, WakeEnvironmentErrors, GetContainerLogsData, GetContainerLogsErrors, ListContainersData, ListContainersResponses, ListContainersErrors, GetContainerDetailData, GetContainerDetailResponses, GetContainerDetailErrors, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerMetricsData, GetContainerMetricsResponses, GetContainerMetricsErrors, StreamContainerMetricsData, StreamContainerMetricsResponses, StreamContainerMetricsErrors, RestartContainerData, RestartContainerResponses, RestartContainerErrors, StartContainerData, StartContainerResponses, StartContainerErrors, StopContainerData, StopContainerResponses, StopContainerErrors, DeployFromImageData, DeployFromImageResponses, DeployFromImageErrors, DeployFromImageUploadData, DeployFromImageUploadResponses, DeployFromImageUploadErrors, DeployFromStaticData, DeployFromStaticResponses, DeployFromStaticErrors, ListAlertRulesData, ListAlertRulesResponses, ListAlertRulesErrors, CreateAlertRuleData, CreateAlertRuleResponses, CreateAlertRuleErrors, DeleteAlertRuleData, DeleteAlertRuleResponses, DeleteAlertRuleErrors, GetAlertRuleData, GetAlertRuleResponses, GetAlertRuleErrors, UpdateAlertRuleData, UpdateAlertRuleResponses, UpdateAlertRuleErrors, GetErrorDashboardStatsData, GetErrorDashboardStatsResponses, GetErrorDashboardStatsErrors, ListErrorGroupsData, ListErrorGroupsResponses, ListErrorGroupsErrors, GetErrorGroupData, GetErrorGroupResponses, GetErrorGroupErrors, UpdateErrorGroupData, UpdateErrorGroupResponses, UpdateErrorGroupErrors, ListErrorEventsData, ListErrorEventsResponses, ListErrorEventsErrors, GetErrorEventData, GetErrorEventResponses, GetErrorEventErrors, GetErrorStatsData, GetErrorStatsResponses, GetErrorStatsErrors, GetErrorTimeSeriesData, GetErrorTimeSeriesResponses, GetErrorTimeSeriesErrors, GetEventsCountData, GetEventsCountResponses, GetEventsCountErrors, GetEventTypeBreakdownData, GetEventTypeBreakdownResponses, GetEventTypeBreakdownErrors, RecordConsoleEventData, RecordConsoleEventResponses, RecordConsoleEventErrors, GetPropertyBreakdownData, GetPropertyBreakdownResponses, GetPropertyBreakdownErrors, GetPropertyTimelineData, GetPropertyTimelineResponses, GetPropertyTimelineErrors, GetEventsTimelineData, GetEventsTimelineResponses, GetEventsTimelineErrors, GetUniqueEventsData, GetUniqueEventsResponses, GetUniqueEventsErrors, ListRemoteExternalImagesData, ListRemoteExternalImagesResponses, ListRemoteExternalImagesErrors, RegisterExternalImageData, RegisterExternalImageResponses, RegisterExternalImageErrors, DeleteExternalImageData, DeleteExternalImageResponses, DeleteExternalImageErrors, GetRemoteExternalImageData, GetRemoteExternalImageResponses, GetRemoteExternalImageErrors, ListFunnelsData, ListFunnelsResponses, ListFunnelsErrors, CreateFunnelData, CreateFunnelResponses, CreateFunnelErrors, PreviewFunnelMetricsData, PreviewFunnelMetricsResponses, PreviewFunnelMetricsErrors, DeleteFunnelData, DeleteFunnelResponses, DeleteFunnelErrors, UpdateFunnelData, UpdateFunnelResponses, UpdateFunnelErrors, GetFunnelMetricsData, GetFunnelMetricsResponses, GetFunnelMetricsErrors, UpdateGitSettingsData, UpdateGitSettingsResponses, UpdateGitSettingsErrors, ReinstallGitlabWebhookData, ReinstallGitlabWebhookResponses, ReinstallGitlabWebhookErrors, HasErrorGroupsData, HasErrorGroupsResponses, HasErrorGroupsErrors, HasAnalyticsEventsData, HasAnalyticsEventsResponses, HasAnalyticsEventsErrors, GetHourlyVisitsData, GetHourlyVisitsResponses, GetHourlyVisitsErrors, ListExternalImagesData, ListExternalImagesResponses, ListExternalImagesErrors, PushExternalImageData, PushExternalImageResponses, PushExternalImageErrors, GetExternalImageData, GetExternalImageResponses, GetExternalImageErrors, ListIncidentsData, ListIncidentsResponses, ListIncidentsErrors, CreateIncidentData, CreateIncidentResponses, CreateIncidentErrors, GetBucketedIncidentsData, GetBucketedIncidentsResponses, GetBucketedIncidentsErrors, PurgeProjectLogsData, PurgeProjectLogsResponses, PurgeProjectLogsErrors, ListMcpsData, ListMcpsResponses, ListMcpsErrors, CreateMcpData, CreateMcpResponses, CreateMcpErrors, DeleteMcpData, DeleteMcpResponses, DeleteMcpErrors, GetMcpData, GetMcpResponses, GetMcpErrors, UpdateMcpData, UpdateMcpResponses, UpdateMcpErrors, ListMonitorsData, ListMonitorsResponses, ListMonitorsErrors, CreateMonitorData, CreateMonitorResponses, CreateMonitorErrors, ObservabilityListEventsData, ObservabilityListEventsResponses, ObservabilityListEventsErrors, ObservabilityFullEventData, ObservabilityFullEventResponses, ObservabilityFullEventErrors, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsResponses, DeleteReleaseSourceMapsErrors, ListSourceMapsData, ListSourceMapsResponses, ListSourceMapsErrors, UploadSourceMapData, UploadSourceMapResponses, UploadSourceMapErrors, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationResponses, RevenueCreateIntegrationErrors, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueUpdateConfigData, RevenueUpdateConfigResponses, RevenueUpdateConfigErrors, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvResponses, RevenueImportInvoicesCsvErrors, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvResponses, RevenueImportSubscriptionsCsvErrors, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateSecretData, RevenueUpdateSecretResponses, RevenueUpdateSecretErrors, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, ListProjectSecretsData, ListProjectSecretsResponses, ListProjectSecretsErrors, CreateProjectSecretData, CreateProjectSecretResponses, CreateProjectSecretErrors, DeleteProjectSecretData, DeleteProjectSecretResponses, DeleteProjectSecretErrors, UpdateProjectSecretData, UpdateProjectSecretResponses, UpdateProjectSecretErrors, UpdateProjectSettingsData, UpdateProjectSettingsResponses, UpdateProjectSettingsErrors, ListSkillsData, ListSkillsResponses, ListSkillsErrors, CreateSkillData, CreateSkillResponses, CreateSkillErrors, UploadSkillData, UploadSkillResponses, UploadSkillErrors, DeleteSkillData, DeleteSkillResponses, DeleteSkillErrors, GetSkillData, GetSkillResponses, GetSkillErrors, UpdateSkillData, UpdateSkillResponses, UpdateSkillErrors, DownloadSkillArchiveData, DownloadSkillArchiveResponses, DownloadSkillArchiveErrors, ListReleasesData, ListReleasesResponses, ListReleasesErrors, DeleteSourceMapData, DeleteSourceMapResponses, DeleteSourceMapErrors, ListStaticBundlesData, ListStaticBundlesResponses, ListStaticBundlesErrors, DeleteStaticBundleData, DeleteStaticBundleResponses, DeleteStaticBundleErrors, GetStaticBundleData, GetStaticBundleResponses, GetStaticBundleErrors, GetStatusOverviewData, GetStatusOverviewResponses, GetStatusOverviewErrors, GetUniqueCountsData, GetUniqueCountsResponses, GetUniqueCountsErrors, UploadStaticBundleData, UploadStaticBundleResponses, UploadStaticBundleErrors, ListProjectScansData, ListProjectScansResponses, ListProjectScansErrors, TriggerScanData, TriggerScanResponses, TriggerScanErrors, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentResponses, GetLatestScansPerEnvironmentErrors, GetLatestScanData, GetLatestScanResponses, GetLatestScanErrors, ListWebhooksData, ListWebhooksResponses, ListWebhooksErrors, CreateWebhookData, CreateWebhookResponses, CreateWebhookErrors, DeleteWebhookData, DeleteWebhookResponses, DeleteWebhookErrors, GetWebhookData, GetWebhookResponses, GetWebhookErrors, UpdateWebhookData, UpdateWebhookResponses, UpdateWebhookErrors, ListDeliveriesData, ListDeliveriesResponses, ListDeliveriesErrors, GetDeliveryData, GetDeliveryResponses, GetDeliveryErrors, RetryDeliveryData, RetryDeliveryResponses, RetryDeliveryErrors, WorkflowDryRunData, WorkflowDryRunResponses, WorkflowDryRunErrors, GetProxyLogsData, GetProxyLogsResponses, GetProxyLogsErrors, GetProxyLogByRequestIdData, GetProxyLogByRequestIdResponses, GetProxyLogByRequestIdErrors, GetProjectsHealthData, GetProjectsHealthResponses, GetProjectsHealthErrors, GetTimeBucketStatsData, GetTimeBucketStatsResponses, GetTimeBucketStatsErrors, GetTodayStatsData, GetTodayStatsResponses, GetTodayStatsErrors, GetProxyLogByIdData, GetProxyLogByIdResponses, GetProxyLogByIdErrors, ListSyncedRepositoriesData, ListSyncedRepositoriesResponses, ListSyncedRepositoriesErrors, GetRepositoryByNameData, GetRepositoryByNameResponses, GetRepositoryByNameErrors, GetAllRepositoriesByNameData, GetAllRepositoriesByNameResponses, GetAllRepositoriesByNameErrors, GetRepositoryPresetByNameData, GetRepositoryPresetByNameResponses, GetRepositoryPresetByNameErrors, GetRepositoryBranchesData, GetRepositoryBranchesResponses, GetRepositoryBranchesErrors, GetRepositoryTagsData, GetRepositoryTagsResponses, GetRepositoryTagsErrors, GetRepositoryPresetLiveData, GetRepositoryPresetLiveResponses, GetRepositoryPresetLiveErrors, GetRepositoryByIdData, GetRepositoryByIdResponses, GetRepositoryByIdErrors, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdResponses, GetBranchesByRepositoryIdErrors, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdResponses, ListCommitsByRepositoryIdErrors, CheckCommitExistsData, CheckCommitExistsResponses, CheckCommitExistsErrors, GetTagsByRepositoryIdData, GetTagsByRepositoryIdResponses, GetTagsByRepositoryIdErrors, GetRestoreRunData, GetRestoreRunResponses, GetRestoreRunErrors, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueListProvidersData, RevenueListProvidersResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysResponses, GetProjectSessionReplaysErrors, GetSessionEventsData, GetSessionEventsResponses, GetSessionEventsErrors, GetSettingsData, GetSettingsResponses, GetSettingsErrors, UpdateSettingsData, UpdateSettingsResponses, UpdateSettingsErrors, SaveAgentTokenData, SaveAgentTokenResponses, SaveAgentTokenErrors, ListAiProvidersData, ListAiProvidersResponses, ListAiProvidersErrors, UpdateAiProviderData, UpdateAiProviderResponses, UpdateAiProviderErrors, ActivateAiProviderData, ActivateAiProviderResponses, ActivateAiProviderErrors, SaveAiProviderCredentialData, SaveAiProviderCredentialResponses, SaveAiProviderCredentialErrors, RevokeJoinTokenData, RevokeJoinTokenResponses, RevokeJoinTokenErrors, GenerateJoinTokenData, GenerateJoinTokenResponses, GenerateJoinTokenErrors, GetJoinTokenStatusData, GetJoinTokenStatusResponses, GetJoinTokenStatusErrors, ListGlobalMcpsData, ListGlobalMcpsResponses, ListGlobalMcpsErrors, CreateGlobalMcpData, CreateGlobalMcpResponses, CreateGlobalMcpErrors, DeleteGlobalMcpData, DeleteGlobalMcpResponses, DeleteGlobalMcpErrors, GetGlobalMcpData, GetGlobalMcpResponses, GetGlobalMcpErrors, UpdateGlobalMcpData, UpdateGlobalMcpResponses, UpdateGlobalMcpErrors, RefreshRouteTableData, RefreshRouteTableResponses, RefreshRouteTableErrors, RebuildSandboxImageData, RebuildSandboxImageResponses, RebuildSandboxImageErrors, GetGlobalSandboxStatusData, GetGlobalSandboxStatusResponses, GetGlobalSandboxStatusErrors, ListSecretsData, ListSecretsResponses, ListSecretsErrors, UpsertSecretData, UpsertSecretResponses, UpsertSecretErrors, DeleteSecretData, DeleteSecretResponses, DeleteSecretErrors, ListGlobalSkillsData, ListGlobalSkillsResponses, ListGlobalSkillsErrors, CreateGlobalSkillData, CreateGlobalSkillResponses, CreateGlobalSkillErrors, UploadGlobalSkillData, UploadGlobalSkillResponses, UploadGlobalSkillErrors, DeleteGlobalSkillData, DeleteGlobalSkillResponses, DeleteGlobalSkillErrors, GetGlobalSkillData, GetGlobalSkillResponses, GetGlobalSkillErrors, UpdateGlobalSkillData, UpdateGlobalSkillResponses, UpdateGlobalSkillErrors, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveResponses, DownloadGlobalSkillArchiveErrors, ListProjectTemplatesData, ListProjectTemplatesResponses, ListProjectTemplatesErrors, ListProjectTemplateTagsData, ListProjectTemplateTagsResponses, ListProjectTemplateTagsErrors, GetProjectTemplateData, GetProjectTemplateResponses, GetProjectTemplateErrors, GetCurrentUserData, GetCurrentUserResponses, GetCurrentUserErrors, ListUsersData, ListUsersResponses, ListUsersErrors, CreateUserData, CreateUserResponses, CreateUserErrors, UpdateSelfData, UpdateSelfResponses, UpdateSelfErrors, DisableMfaData, DisableMfaResponses, DisableMfaErrors, SetupMfaData, SetupMfaResponses, SetupMfaErrors, VerifyAndEnableMfaData, VerifyAndEnableMfaResponses, VerifyAndEnableMfaErrors, ChangePasswordSelfData, ChangePasswordSelfResponses, ChangePasswordSelfErrors, DeleteUserData, DeleteUserResponses, DeleteUserErrors, UpdateUserData, UpdateUserResponses, UpdateUserErrors, RestoreUserData, RestoreUserResponses, RestoreUserErrors, AssignRoleData, AssignRoleResponses, AssignRoleErrors, RemoveRoleData, RemoveRoleResponses, RemoveRoleErrors, ListSandboxesData, ListSandboxesResponses, CreateSandboxData, CreateSandboxResponses, CreateSandboxErrors, GetSandboxData, GetSandboxResponses, GetSandboxErrors, CmdData, CmdResponses, CmdErrors, GetCmdData, GetCmdResponses, GetCmdErrors, CmdLogsData, CmdLogsResponses, CmdLogsErrors, DestroySandboxData, DestroySandboxResponses, DestroySandboxErrors, DomainData, DomainResponses, DomainErrors, ExecData, ExecResponses, ExecErrors, ExecDetachedData, ExecDetachedResponses, ExecDetachedErrors, ExtendTimeoutData, ExtendTimeoutResponses, ExtendTimeoutErrors, MkdirData, MkdirResponses, MkdirErrors, ReadFileData, ReadFileResponses, ReadFileErrors, StatPathData, StatPathResponses, StatPathErrors, WriteFileData, WriteFileResponses, WriteFileErrors, WriteFilesData, WriteFilesResponses, WriteFilesErrors, ListJobsData, ListJobsResponses, ListJobsErrors, JobStatusData, JobStatusResponses, JobStatusErrors, KillJobData, KillJobResponses, KillJobErrors, JobLogsData, JobLogsResponses, JobLogsErrors, PauseSandboxData, PauseSandboxResponses, PauseSandboxErrors, ClearPreviewPasswordData, ClearPreviewPasswordResponses, ClearPreviewPasswordErrors, SetPreviewPasswordData, SetPreviewPasswordResponses, SetPreviewPasswordErrors, RestartSandboxData, RestartSandboxResponses, RestartSandboxErrors, ResumeSandboxData, ResumeSandboxResponses, ResumeSandboxErrors, SourceSandboxData, SourceSandboxResponses, SourceSandboxErrors, StopSandboxData, StopSandboxResponses, StopSandboxErrors, CmdKillData, CmdKillResponses, CmdKillErrors, GetVisitorSessionsData, GetVisitorSessionsResponses, GetVisitorSessionsErrors, DeleteSessionReplayData, DeleteSessionReplayResponses, DeleteSessionReplayErrors, GetSessionReplayData, GetSessionReplayResponses, GetSessionReplayErrors, UpdateSessionDurationData, UpdateSessionDurationResponses, UpdateSessionDurationErrors, GetSessionReplayEventsData, GetSessionReplayEventsResponses, GetSessionReplayEventsErrors, AddEventsData, AddEventsResponses, AddEventsErrors, DeleteScanData, DeleteScanResponses, DeleteScanErrors, GetScanData, GetScanResponses, GetScanErrors, GetScanVulnerabilitiesData, GetScanVulnerabilitiesResponses, GetScanVulnerabilitiesErrors, ListEventTypesData, ListEventTypesResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestResponses, TriggerWeeklyDigestErrors, ListExternalPluginsData, ListExternalPluginsResponses, ReloadPluginsData, ReloadPluginsResponses, ReloadPluginsErrors, IngestSentryEnvelopeData, IngestSentryEnvelopeResponses, IngestSentryEnvelopeErrors, IngestSentryEventData, IngestSentryEventResponses, IngestSentryEventErrors, ListAuditLogsData, ListAuditLogsResponses, ListAuditLogsErrors, GetAuditLogData, GetAuditLogResponses, GetAuditLogErrors } from './types.gen'; import { client } from './client.gen'; export type Options = ClientOptions & { @@ -1176,6 +1176,23 @@ export const listExternalServiceBackups = }); }; +/** + * List the schedules that target a specific external service. Useful for + * the service detail page ("which schedules back this DB up?"). + */ +export const listServiceSchedules = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/backups/external-services/{service_id}/schedules', + ...options + }); +}; + /** * List all S3 sources */ @@ -1584,6 +1601,62 @@ export const listScheduleRuns = (options: }); }; +/** + * List the external services attached to a backup schedule. + */ +export const listScheduleServices = (options: Options) => { + return (options.client ?? client).get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/backups/schedules/{id}/services', + ...options + }); +}; + +/** + * Attach one or more external services to a backup schedule. Idempotent — + * services that are already attached are silently skipped (`ON CONFLICT + * DO NOTHING`). Returns the count of newly inserted rows + the total + * membership after the operation. + */ +export const attachScheduleServices = (options: Options) => { + return (options.client ?? client).post({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/backups/schedules/{id}/services', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Detach a single external service from a backup schedule. Idempotent — + * returns `204` whether or not a row was actually removed. + */ +export const detachScheduleService = (options: Options) => { + return (options.client ?? client).delete({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/backups/schedules/{id}/services/{service_id}', + ...options + }); +}; + /** * Get a backup by ID */ diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index 5ae637a3..868ecbb8 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -639,6 +639,32 @@ export type AssignRoleRequest = { user_id: number; }; +/** + * Body for `POST /api/backups/schedules/{id}/services` — attach external + * services to a backup schedule. Idempotent. + */ +export type AttachScheduleServicesRequest = { + /** + * External service ids to attach. Duplicates are de-duplicated server-side. + */ + service_ids: Array; +}; + +/** + * Response for `POST /api/backups/schedules/{id}/services`. + */ +export type AttachScheduleServicesResponse = { + /** + * Number of rows actually inserted (excludes rows skipped by + * `ON CONFLICT DO NOTHING`). + */ + inserted: number; + /** + * Total number of services now attached to the schedule. + */ + total_attached: number; +}; + /** * IP address information in audit log */ @@ -950,6 +976,12 @@ export type BackupScheduleResponse = { description?: string | null; enabled: boolean; id: number; + /** + * When `true`, every run also produces a `control_plane` backup + * (Temps's own Postgres). When `false`, only the external service + * fan-out happens. + */ + include_control_plane: boolean; last_run?: number | null; /** * Per-schedule wall-clock timeout override for backup jobs (seconds). @@ -963,6 +995,12 @@ export type BackupScheduleResponse = { s3_source_id: number; schedule_expression: string; tags: Array; + /** + * When `true`, the schedule auto-includes every external service on + * the host (and any future ones). When `false`, the schedule only + * targets services attached via `backup_schedule_services`. + */ + target_all_services: boolean; updated_at: number; }; @@ -2131,6 +2169,13 @@ export type CreateBackupScheduleRequest = { backup_type: string; description?: string | null; enabled: boolean; + /** + * When `true` (default), every run also produces a `control_plane` + * backup of Temps's own database. Operators who use Temps purely as + * a backup orchestrator for external DBs can set this to `false` to + * keep the run history focused on those services. + */ + include_control_plane?: boolean | null; /** * Optional wall-clock timeout override for jobs created by this schedule * (seconds). When set, overrides the engine-family default. `null` means @@ -2146,6 +2191,13 @@ export type CreateBackupScheduleRequest = { s3_source_id?: number | null; schedule_expression: string; tags: Array; + /** + * When `true` (default), the schedule backs up every external service + * on the host — including databases created in the future. When + * `false`, the schedule backs up only the services explicitly attached + * via `POST /backups/schedules/{id}/services`. Omit to use the default. + */ + target_all_services?: boolean | null; }; export type CreateDsnRequest = { @@ -12925,6 +12977,10 @@ export type UpdateBackupScheduleRequest = { * Enable or disable the schedule. Skipped when `None`. */ enabled?: boolean | null; + /** + * Toggle whether the control-plane backup is produced on every run. + */ + include_control_plane?: boolean | null; /** * Per-schedule wall-clock timeout override (seconds). * @@ -12949,6 +13005,12 @@ export type UpdateBackupScheduleRequest = { * Replace the full tag list. Skipped when `None`. */ tags?: Array | null; + /** + * Toggle between "back up every database" (`true`) and "back up only + * the explicit list" (`false`). When set to `true`, the server clears + * the explicit membership rows for this schedule. + */ + target_all_services?: boolean | null; }; /** @@ -17610,6 +17672,48 @@ export type ListExternalServiceBackupsResponses = { export type ListExternalServiceBackupsResponse = ListExternalServiceBackupsResponses[keyof ListExternalServiceBackupsResponses]; +export type ListServiceSchedulesData = { + body?: never; + path: { + /** + * External service ID + */ + service_id: number; + }; + query?: never; + url: '/backups/external-services/{service_id}/schedules'; +}; + +export type ListServiceSchedulesErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Service not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type ListServiceSchedulesError = ListServiceSchedulesErrors[keyof ListServiceSchedulesErrors]; + +export type ListServiceSchedulesResponses = { + /** + * Schedules backing up this service + */ + 200: Array; +}; + +export type ListServiceSchedulesResponse = ListServiceSchedulesResponses[keyof ListServiceSchedulesResponses]; + export type ListS3SourcesData = { body?: never; path?: never; @@ -18354,6 +18458,136 @@ export type ListScheduleRunsResponses = { export type ListScheduleRunsResponse = ListScheduleRunsResponses[keyof ListScheduleRunsResponses]; +export type ListScheduleServicesData = { + body?: never; + path: { + /** + * Schedule ID + */ + id: number; + }; + query?: never; + url: '/backups/schedules/{id}/services'; +}; + +export type ListScheduleServicesErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Schedule not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type ListScheduleServicesError = ListScheduleServicesErrors[keyof ListScheduleServicesErrors]; + +export type ListScheduleServicesResponses = { + /** + * Services attached to this schedule + */ + 200: Array; +}; + +export type ListScheduleServicesResponse = ListScheduleServicesResponses[keyof ListScheduleServicesResponses]; + +export type AttachScheduleServicesData = { + body: AttachScheduleServicesRequest; + path: { + /** + * Schedule ID + */ + id: number; + }; + query?: never; + url: '/backups/schedules/{id}/services'; +}; + +export type AttachScheduleServicesErrors = { + /** + * Validation error + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Schedule not found + */ + 404: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type AttachScheduleServicesError = AttachScheduleServicesErrors[keyof AttachScheduleServicesErrors]; + +export type AttachScheduleServicesResponses = { + /** + * Services attached + */ + 200: AttachScheduleServicesResponse; +}; + +export type AttachScheduleServicesResponse2 = AttachScheduleServicesResponses[keyof AttachScheduleServicesResponses]; + +export type DetachScheduleServiceData = { + body?: never; + path: { + /** + * Schedule ID + */ + id: number; + /** + * External service ID + */ + service_id: number; + }; + query?: never; + url: '/backups/schedules/{id}/services/{service_id}'; +}; + +export type DetachScheduleServiceErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Internal server error + */ + 500: ProblemDetails; +}; + +export type DetachScheduleServiceError = DetachScheduleServiceErrors[keyof DetachScheduleServiceErrors]; + +export type DetachScheduleServiceResponses = { + /** + * Service detached (or was not attached) + */ + 204: void; +}; + +export type DetachScheduleServiceResponse = DetachScheduleServiceResponses[keyof DetachScheduleServiceResponses]; + export type GetBackupData = { body?: never; path: { diff --git a/web/src/components/backups/ScheduleServicesSelector.tsx b/web/src/components/backups/ScheduleServicesSelector.tsx new file mode 100644 index 00000000..b15919d3 --- /dev/null +++ b/web/src/components/backups/ScheduleServicesSelector.tsx @@ -0,0 +1,127 @@ +'use client' + +/** + * Picker for selecting external services to back up. Used by: + * - CreateBackupSchedule (initial selection) + * - ScheduleDetail (attach more) + * + * Behaviour: + * - Shows every external service the host knows about, ordered by name. + * - A "Select all" master toggle that's checked by default for create + * flows (matches the user expectation: "back up all my DBs"). + * - Caller controls the `value` (selected service ids) so this stays + * dumb and reusable. + * + * The `excludeIds` prop hides already-attached services on the detail page. + */ + +import { Checkbox } from '@/components/ui/checkbox' +import { Skeleton } from '@/components/ui/skeleton' +import { listServicesOptions } from '@/api/client/@tanstack/react-query.gen' +import { useQuery } from '@tanstack/react-query' +import { Database, HardDrive } from 'lucide-react' + +interface Props { + /** Selected service ids. */ + value: number[] + /** Called with the new selection on every change. */ + onChange: (next: number[]) => void + /** Hide these service ids (e.g. already attached). */ + excludeIds?: number[] + /** Disable interaction (during a mutation). */ + disabled?: boolean +} + +export function ScheduleServicesSelector({ + value, + onChange, + excludeIds = [], + disabled = false, +}: Props) { + const { data: services, isPending } = useQuery({ + ...listServicesOptions({ query: { page_size: 100 } }), + }) + + const visible = (services ?? []).filter( + (s) => !excludeIds.includes(s.id), + ) + const allSelected = + visible.length > 0 && visible.every((s) => value.includes(s.id)) + const someSelected = visible.some((s) => value.includes(s.id)) + + function toggleAll() { + if (allSelected) { + onChange(value.filter((id) => !visible.some((s) => s.id === id))) + } else { + const next = new Set(value) + visible.forEach((s) => next.add(s.id)) + onChange(Array.from(next)) + } + } + + function toggleOne(id: number) { + if (value.includes(id)) { + onChange(value.filter((v) => v !== id)) + } else { + onChange([...value, id]) + } + } + + if (isPending) { + return ( +
+ + + +
+ ) + } + + if (visible.length === 0) { + return ( +
+ No external services found. Add a Postgres, Redis, MongoDB, or RustFS + service first and they'll appear here. +
+ ) + } + + return ( +
+ +
+
+ {visible.map((svc) => { + const checked = value.includes(svc.id) + const Icon = svc.service_type === 's3' ? HardDrive : Database + return ( + + ) + })} +
+
+ ) +} diff --git a/web/src/favicon.png b/web/src/favicon.png index 37759a81..36bddd30 100644 Binary files a/web/src/favicon.png and b/web/src/favicon.png differ diff --git a/web/src/lib/backup-schedules.ts b/web/src/lib/backup-schedules.ts deleted file mode 100644 index 4e6ec8e5..00000000 --- a/web/src/lib/backup-schedules.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Hand-written helper for the PATCH /api/backups/schedules/{id} endpoint. - * - * TODO(sdk-regen): replace with generated updateBackupSchedule mutation after - * next `bun run openapi-ts` run against a server that exposes this endpoint. - */ - -/** - * Patch body for `PATCH /api/backups/schedules/{id}`. - * - * All fields are optional; only fields that are present in the object sent to - * the API will be updated. Omit a field entirely to leave its column unchanged. - * - * Note: null-clearing `max_runtime_secs` is not supported via PATCH — send a - * positive integer to set, or omit to leave unchanged. To clear it, disable - * the schedule and recreate it. - */ -export interface UpdateBackupScheduleRequest { - /** New display name. Must not be empty if present. */ - name?: string - /** New description. Pass `""` to clear the existing description. */ - description?: string - /** - * New cron expression. When changed the server recomputes `next_run`. - * Must have runs at least 1 hour apart. - */ - schedule_expression?: string - /** Days to retain backups. Must be >= 1. */ - retention_period?: number - /** - * Wall-clock timeout in seconds. Must be >= 60. - * Send a positive integer to set; omit to leave unchanged. - */ - max_runtime_secs?: number - /** Enable or disable the schedule. */ - enabled?: boolean - /** Replaces the full tag list when present. */ - tags?: string[] -} - -async function readJsonOrThrow(response: Response): Promise { - if (!response.ok) { - let detail = response.statusText - try { - const body = (await response.json()) as { detail?: string; title?: string } - detail = body.detail ?? body.title ?? detail - } catch { - // fall through with statusText - } - throw new Error(detail) - } - return (await response.json()) as T -} - -/** - * Apply a partial update to an existing backup schedule. - * - * Only fields present in `body` are applied; absent fields leave the - * corresponding column unchanged. On success, returns the updated schedule - * object (same shape as `BackupScheduleResponse` from the generated SDK). - */ -export async function updateBackupSchedule( - id: number, - body: UpdateBackupScheduleRequest, -): Promise { - const response = await fetch(`/api/backups/schedules/${id}`, { - method: 'PATCH', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - return readJsonOrThrow(response) -} diff --git a/web/src/pages/AiGateway.tsx b/web/src/pages/AiGateway.tsx index 8cd6a435..132de5af 100644 --- a/web/src/pages/AiGateway.tsx +++ b/web/src/pages/AiGateway.tsx @@ -82,8 +82,11 @@ import { TrendingUp, DollarSign, Bot, + ChevronLeft, ChevronRight, ArrowLeft, + SlidersHorizontal, + X, Wrench, MessageSquare, AlertTriangle, @@ -168,6 +171,11 @@ interface UsageLogEntry { is_byok: boolean } +interface UsageLogPage { + entries: UsageLogEntry[] + total: number +} + interface ModelPricing { model: string provider: string @@ -334,12 +342,82 @@ function UsageAnalytics() { ), }) - const { data: recentLogs } = useQuery({ - queryKey: ['aiUsage', 'recent'], + // Recent Requests: pagination + filters. `pageSize` is user-configurable up + // to the backend max of 50. Filters reset the page back to 0 on change. + const [recentPage, setRecentPage] = useState(0) + const [recentPageSize, setRecentPageSize] = useState(20) + const [recentProvider, setRecentProvider] = useState('all') + const [recentStatus, setRecentStatus] = useState('all') + const [recentCostOp, setRecentCostOp] = useState<'gte' | 'gt' | 'lte' | 'lt'>('gte') + const [recentCostInput, setRecentCostInput] = useState('') + const [recentTokensOp, setRecentTokensOp] = useState<'gte' | 'gt' | 'lte' | 'lt'>('gte') + const [recentTokensInput, setRecentTokensInput] = useState('') + // The filter row is collapsed by default and only revealed on demand. + const [recentFiltersOpen, setRecentFiltersOpen] = useState(false) + + // Cost is entered by the user in dollars; the API expects microcents. + const recentCostMicrocents = useMemo(() => { + const dollars = parseFloat(recentCostInput) + if (!Number.isFinite(dollars) || dollars < 0) return undefined + return Math.round(dollars * 1_000_000 * 100) + }, [recentCostInput]) + + // Tokens are entered as a plain integer count. + const recentTokensValue = useMemo(() => { + const n = parseInt(recentTokensInput, 10) + if (!Number.isFinite(n) || n < 0) return undefined + return n + }, [recentTokensInput]) + + const recentFilterParams = useMemo(() => { + const params: Record = {} + if (recentProvider !== 'all') params.provider = recentProvider + if (recentStatus !== 'all') params.status = recentStatus + if (recentCostMicrocents !== undefined) { + params[`cost_${recentCostOp}`] = String(recentCostMicrocents) + } + if (recentTokensValue !== undefined) { + params[`tokens_${recentTokensOp}`] = String(recentTokensValue) + } + return params + }, [ + recentProvider, + recentStatus, + recentCostOp, + recentCostMicrocents, + recentTokensOp, + recentTokensValue, + ]) + + const recentActiveFilterCount = Object.keys(recentFilterParams).length + + const { data: recentLogsPage, isPlaceholderData: recentIsPlaceholder } = useQuery({ + queryKey: ['aiUsage', 'recent', recentPage, recentPageSize, recentFilterParams], queryFn: () => - fetchJson(buildUsageUrl('recent', { limit: '20' })), + fetchJson( + buildUsageUrl('recent', { + limit: String(recentPageSize), + offset: String(recentPage * recentPageSize), + ...recentFilterParams, + }) + ), + placeholderData: (prev) => prev, }) + const recentLogs = recentLogsPage?.entries + const recentTotal = recentLogsPage?.total ?? 0 + const recentTotalPages = Math.max(1, Math.ceil(recentTotal / recentPageSize)) + const recentHasFilters = recentActiveFilterCount > 0 + + // Reset to the first page whenever filters or page size change, so the user + // never lands on an out-of-range page after narrowing the result set. + const recentResetKey = `${recentPageSize}|${JSON.stringify(recentFilterParams)}` + const [recentLastResetKey, setRecentLastResetKey] = useState(recentResetKey) + if (recentResetKey !== recentLastResetKey) { + setRecentLastResetKey(recentResetKey) + setRecentPage(0) + } + const { data: pricingData } = useQuery({ queryKey: ['aiPricing'], queryFn: () => fetchJson('/api/ai/pricing'), @@ -633,16 +711,151 @@ function UsageAnalytics() { {/* Recent requests */} - - Recent Requests + +
+ Recent Requests + +
+ {/* Filter row — hidden until the user opens it, or while a filter is active */} + {(recentFiltersOpen || recentHasFilters) && ( +
+ + +
+ +
+ + $ + + setRecentCostInput(e.target.value)} + className="pl-6" + /> +
+
+
+ + setRecentTokensInput(e.target.value)} + className="w-full sm:w-[100px]" + /> +
+ {recentHasFilters && ( + + )} +
+ )}
{!recentLogs || recentLogs.length === 0 ? (
- No recent requests + {recentHasFilters ? 'No requests match these filters' : 'No recent requests'}
) : ( -
+
@@ -709,6 +922,57 @@ function UsageAnalytics() {
)} + {recentTotal > 0 && ( +
+
+

+ + Showing {recentPage * recentPageSize + 1}– + {Math.min((recentPage + 1) * recentPageSize, recentTotal)} of{' '} + {recentTotal.toLocaleString()} + + + {recentPage + 1} / {recentTotalPages} + +

+ +
+
+ + +
+
+ )}
diff --git a/web/src/pages/CliLogin.tsx b/web/src/pages/CliLogin.tsx index 3b0ceab2..ae32d5c1 100644 --- a/web/src/pages/CliLogin.tsx +++ b/web/src/pages/CliLogin.tsx @@ -320,10 +320,15 @@ async function jsonRequest( return (await res.json()) as T } +// The auth plugin's routes are nested under `/api` by `temps-core` +// (see `temps-core/src/plugin.rs`), so these endpoints live at +// `/api/auth/cli/device/{lookup,approve,deny}` — NOT at the server root. +// Hitting the unprefixed paths hits the SPA catch-all and returns HTML, +// which silently leaves the page stuck on "unknown" with no Authorize button. async function fetchDeviceLookup(userCode: string): Promise { return jsonRequest( 'GET', - `/auth/cli/device/lookup?user_code=${encodeURIComponent(userCode)}`, + `/api/auth/cli/device/lookup?user_code=${encodeURIComponent(userCode)}`, ) } @@ -331,7 +336,7 @@ async function postDeviceAction( action: 'approve' | 'deny', userCode: string, ): Promise<{ user_code: string; status: string }> { - return jsonRequest('POST', `/auth/cli/device/${action}`, { + return jsonRequest('POST', `/api/auth/cli/device/${action}`, { user_code: userCode, }) } diff --git a/web/src/pages/CreateBackupSchedule.tsx b/web/src/pages/CreateBackupSchedule.tsx index bf832ce9..bb1f122f 100644 --- a/web/src/pages/CreateBackupSchedule.tsx +++ b/web/src/pages/CreateBackupSchedule.tsx @@ -11,9 +11,11 @@ */ import { + attachScheduleServicesMutation, createBackupScheduleMutation, getS3SourceOptions, } from '@/api/client/@tanstack/react-query.gen' +import { ScheduleServicesSelector } from '@/components/backups/ScheduleServicesSelector' import { Button } from '@/components/ui/button' import { Card, @@ -26,6 +28,7 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Switch } from '@/components/ui/switch' import { Select, SelectContent, @@ -77,6 +80,20 @@ export function CreateBackupSchedule() { scheduleOptions[1].value, ) const [customCron, setCustomCron] = useState('') + // 'all' (default) means the schedule backs up every database — including + // ones created after this schedule. 'specific' means it only backs up the + // services explicitly picked below. Default chosen to match what most + // operators want: "back up everything, even when I add a new DB later." + const [backupMode, setBackupMode] = useState<'all' | 'specific'>('all') + const [selectedServiceIds, setSelectedServiceIds] = useState([]) + // Default on: most operators want the control plane covered. Operators + // who only use Temps to orchestrate external DB backups can flip it off. + const [includeControlPlane, setIncludeControlPlane] = useState(true) + + const attachMutation = useMutation({ + ...attachScheduleServicesMutation(), + meta: { errorTitle: 'Failed to attach services to schedule' }, + }) // The mutation's generated error type is `ProblemDetails`. Adding an // explicit `onError: (err: unknown) => ...` widens that and breaks the @@ -86,7 +103,26 @@ export function CreateBackupSchedule() { const createMutation = useMutation({ ...createBackupScheduleMutation(), meta: { errorTitle: 'Failed to create backup schedule' }, - onSuccess: () => { + onSuccess: async (created) => { + // In 'specific' mode, attach the picked services. In 'all' mode there + // is nothing to attach — the schedule's `target_all_services` flag is + // already set on the backend and the fan-out picks every DB at run + // time. + if (backupMode === 'specific' && selectedServiceIds.length > 0) { + try { + await attachMutation.mutateAsync({ + path: { id: created.id }, + body: { service_ids: selectedServiceIds }, + }) + } catch { + // Toast already raised by mutation meta; surface partial success. + toast.warning( + 'Schedule created, but attaching services failed. You can retry from the schedule detail page.', + ) + navigate(`/backups/s3-sources/${id}`) + return + } + } toast.success('Backup schedule created successfully') navigate(`/backups/s3-sources/${id}`) }, @@ -136,6 +172,26 @@ export function CreateBackupSchedule() { max_runtime_secs = Math.round(Number(form.max_runtime_hours) * 3600) } + if (backupMode === 'specific' && selectedServiceIds.length === 0) { + toast.error( + 'Select at least one database, or switch back to "All databases."', + ) + return + } + if ( + backupMode === 'specific' && + selectedServiceIds.length === 0 && + !includeControlPlane + ) { + toast.error( + 'This schedule would have nothing to back up. Enable the control plane or pick at least one database.', + ) + return + } + if (backupMode === 'all' && !includeControlPlane) { + // Allowed (all DBs covered), nothing to block here. + } + createMutation.mutate({ body: { name: form.name, @@ -147,6 +203,8 @@ export function CreateBackupSchedule() { enabled: form.enabled ?? true, tags: [], max_runtime_secs, + target_all_services: backupMode === 'all', + include_control_plane: includeControlPlane, }, }) } @@ -272,6 +330,81 @@ export function CreateBackupSchedule() { />
+
+ + setBackupMode(v as 'all' | 'specific')} + className="gap-4" + > +
+ +
+ +

+ Back up every database currently on the host — and any + new database you create later, automatically. +

+
+
+
+ +
+ +

+ Pick the databases this schedule should back up. New + databases are not included unless you attach them. +

+
+
+
+ {backupMode === 'specific' && ( +
+ +
+ )} +
+
+ +

+ Includes Temps's own database (users, projects, service + configs, audit logs, error groups). Recommended unless you + use Temps purely as a backup orchestrator for external + databases. +

+
+ +
+
+
Cancel - diff --git a/web/src/pages/EditBackupSchedule.tsx b/web/src/pages/EditBackupSchedule.tsx index bfbc3b9a..786364c2 100644 --- a/web/src/pages/EditBackupSchedule.tsx +++ b/web/src/pages/EditBackupSchedule.tsx @@ -14,9 +14,16 @@ */ import { + attachScheduleServicesMutation, + detachScheduleServiceMutation, getBackupScheduleOptions, getS3SourceOptions, + listScheduleServicesOptions, + listScheduleServicesQueryKey, + updateBackupScheduleMutation, } from '@/api/client/@tanstack/react-query.gen' +import type { UpdateBackupScheduleRequest } from '@/api/client/types.gen' +import { ScheduleServicesSelector } from '@/components/backups/ScheduleServicesSelector' import { Button } from '@/components/ui/button' import { Card, @@ -30,12 +37,9 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Skeleton } from '@/components/ui/skeleton' +import { Switch } from '@/components/ui/switch' import { useBreadcrumbs } from '@/contexts/BreadcrumbContext' import { usePageTitle } from '@/hooks/usePageTitle' -import { - UpdateBackupScheduleRequest, - updateBackupSchedule, -} from '@/lib/backup-schedules' import { scheduleOptions } from '@/lib/schedule-options' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ArrowLeft } from 'lucide-react' @@ -77,8 +81,20 @@ export function EditBackupSchedule() { scheduleOptions[1].value, ) const [customCron, setCustomCron] = useState('') + // Backup targets: 'all' covers every DB (including future ones); + // 'specific' uses the explicit selection below. + const [backupMode, setBackupMode] = useState<'all' | 'specific'>('all') + const [selectedServiceIds, setSelectedServiceIds] = useState([]) + const [includeControlPlane, setIncludeControlPlane] = useState(true) const [seeded, setSeeded] = useState(false) + // Load the current explicit membership so we can diff it on save. + // Always fetched — cheap, and lets the user flip modes without a reload. + const { data: attachedServices } = useQuery({ + ...listScheduleServicesOptions({ path: { id: scheduleIdNum! } }), + enabled: !!scheduleIdNum, + }) + // Seed form state from the loaded schedule (once). useEffect(() => { if (!schedule || seeded) return @@ -91,6 +107,8 @@ export function EditBackupSchedule() { : '', ) setEnabled(schedule.enabled) + setBackupMode(schedule.target_all_services ? 'all' : 'specific') + setIncludeControlPlane(schedule.include_control_plane) const preset = scheduleOptions.find( (o) => !o.customizable && o.value === schedule.schedule_expression, ) @@ -99,6 +117,15 @@ export function EditBackupSchedule() { setSeeded(true) }, [schedule, seeded]) + // Once we know the current explicit list, seed the picker with it. We + // only do this the first time the list arrives so user edits stick. + const [seededServices, setSeededServices] = useState(false) + useEffect(() => { + if (seededServices || !attachedServices) return + setSelectedServiceIds(attachedServices.map((s) => s.id)) + setSeededServices(true) + }, [attachedServices, seededServices]) + useEffect(() => { setBreadcrumbs([ { label: 'Backups', href: '/backups' }, @@ -112,21 +139,67 @@ export function EditBackupSchedule() { usePageTitle(schedule ? `Edit — ${schedule.name}` : 'Edit Schedule') + const attachMutation = useMutation({ + ...attachScheduleServicesMutation(), + meta: { errorTitle: 'Failed to attach services' }, + }) + const detachMutation = useMutation({ + ...detachScheduleServiceMutation(), + meta: { errorTitle: 'Failed to detach service' }, + }) + const mutation = useMutation({ - mutationFn: (body: UpdateBackupScheduleRequest) => - updateBackupSchedule(scheduleIdNum!, body), - onSuccess: () => { + ...updateBackupScheduleMutation(), + meta: { errorTitle: 'Failed to update schedule' }, + onSuccess: async () => { + // If we're in 'specific' mode, diff the current vs. desired + // membership and apply attach/detach calls. The backend already + // cleared the join table when the user flipped to 'all' mode, so + // there's nothing to do for that branch. + if (backupMode === 'specific' && attachedServices) { + const current = new Set(attachedServices.map((s) => s.id)) + const desired = new Set(selectedServiceIds) + const toAttach = [...desired].filter((id) => !current.has(id)) + const toDetach = [...current].filter((id) => !desired.has(id)) + + try { + if (toAttach.length > 0) { + await attachMutation.mutateAsync({ + path: { id: scheduleIdNum! }, + body: { service_ids: toAttach }, + }) + } + for (const sid of toDetach) { + await detachMutation.mutateAsync({ + path: { id: scheduleIdNum!, service_id: sid }, + }) + } + } catch { + toast.warning( + 'Schedule saved, but updating backup targets failed. You can retry from the schedule detail page.', + ) + void queryClient.invalidateQueries({ + queryKey: listScheduleServicesQueryKey({ + path: { id: scheduleIdNum! }, + }), + }) + navigate(`/backups/s3-sources/${id}`) + return + } + } + toast.success('Backup schedule updated') void queryClient.invalidateQueries({ queryKey: ['list-backup-schedules'], }) void queryClient.invalidateQueries({ queryKey: ['BackupSchedules'] }) + void queryClient.invalidateQueries({ + queryKey: listScheduleServicesQueryKey({ + path: { id: scheduleIdNum! }, + }), + }) navigate(`/backups/s3-sources/${id}`) }, - onError: (err: unknown) => { - const message = err instanceof Error ? err.message : 'Update failed' - toast.error('Failed to update schedule', { description: message }) - }, }) if (!sourceId || !scheduleIdNum) { @@ -191,7 +264,32 @@ export function EditBackupSchedule() { body.max_runtime_secs = newMaxSecs } - mutation.mutate(body) + const desiredAll = backupMode === 'all' + if (desiredAll !== schedule.target_all_services) { + body.target_all_services = desiredAll + } + if (includeControlPlane !== schedule.include_control_plane) { + body.include_control_plane = includeControlPlane + } + + if (backupMode === 'specific' && selectedServiceIds.length === 0) { + toast.error( + 'Select at least one database, or switch back to "All databases."', + ) + return + } + if ( + backupMode === 'specific' && + selectedServiceIds.length === 0 && + !includeControlPlane + ) { + toast.error( + 'This schedule would have nothing to back up. Enable the control plane or pick at least one database.', + ) + return + } + + mutation.mutate({ path: { id: scheduleIdNum! }, body }) } return ( @@ -319,6 +417,91 @@ export function EditBackupSchedule() { )}
+
+ + + setBackupMode(v as 'all' | 'specific') + } + className="gap-4" + > +
+ +
+ +

+ Back up every database currently on the host — + and any new database you create later, automatically. +

+
+
+
+ +
+ +

+ Pick the databases this schedule should back up. + New databases are not included unless you attach + them. +

+
+
+
+ {backupMode === 'specific' && ( +
+ +
+ )} +
+
+ +

+ Includes Temps's own database (users, projects, + service configs, audit logs, error groups). Recommended + unless you use Temps purely as a backup orchestrator + for external databases. +

+
+ +
+
+
)} +
+
+ Backup targets +
+
+ {s.target_all_services ? ( + + All databases{' '} + + (includes future databases automatically) + + + ) : ( + Specific databases (configured below) + )} +
+
+
+
+ Control plane backup +
+
+ {s.include_control_plane ? ( + + Included{' '} + + (Temps's own database is backed up every run) + + + ) : ( + + Skipped{' '} + + (only external services are backed up) + + + )} +
+
@@ -550,6 +643,115 @@ export function ScheduleDetail() { {/* ── Config card ── */} {renderScheduleConfigCard(schedule)} + {/* ── Backup targets card ── */} + {/* + * In 'all databases' mode the join table is irrelevant — the + * fan-out targets every external service at run time. Show a + * hint instead of the attach/detach UI to avoid implying that + * any of those buttons would change behaviour. In 'specific' + * mode we surface the editable list. + */} + {schedule.target_all_services ? ( + + + + + Backup targets + + + This schedule backs up every database on the host. New + databases are automatically included on the next run. + + + +
+ To restrict this schedule to a specific list of databases, + edit the schedule and switch to Specific + databases. +
+
+
+ ) : ( + + +
+ + + Backup targets + + + External services this schedule backs up on every run. + Currently in specific mode — only the + listed services are included. + +
+ +
+ + {isLoadingServices ? ( +
+ + +
+ ) : !attachedServices || attachedServices.length === 0 ? ( +
+ No services attached yet. Click Attach service{' '} + to add Postgres, Redis, MongoDB, or RustFS targets. +
+ ) : ( +
    + {attachedServices.map((svc) => { + const Icon = svc.service_type === 's3' ? HardDrive : Database + return ( +
  • + + {svc.name} + + {svc.service_type} + + +
  • + ) + })} +
+ )} +
+
+ )} + {/* ── Run history table ── */} @@ -637,6 +839,50 @@ export function ScheduleDetail() {
+ {/* ── Attach services dialog ── */} + + + + Attach services + + Pick the external services to add to this schedule. Already- + attached services are hidden. + + + s.id) ?? []} + disabled={attachMutation.isPending} + /> + + + + + + + {/* ── Delete confirmation dialog ── */}