From 7a869200678a57290ef68b8b81fbcc7b3cee8f37 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sat, 28 Feb 2026 21:39:40 -0500 Subject: [PATCH 1/4] feat: add VitePress documentation site Comprehensive docs site covering authentication, configuration, deployment, architecture, and extension points. Organized into user-facing guide and admin-facing sections. Styled to match docs.source.coop visual identity (IBM Plex Sans, Cascadia Mono headings, warm off-white/teal-gray color scheme). Co-Authored-By: Claude Opus 4.6 --- .claude/launch.json | 12 + .gitignore | 3 + docs/.vitepress/config.ts | 165 ++ docs/.vitepress/theme/index.ts | 4 + docs/.vitepress/theme/style.css | 220 ++ docs/architecture/crate-layout.md | 114 + docs/architecture/index.md | 57 + docs/architecture/multi-runtime.md | 78 + docs/architecture/request-lifecycle.md | 95 + docs/auth/backend-auth.md | 255 ++ docs/auth/index.md | 46 + docs/auth/proxy-auth.md | 353 +++ docs/auth/sealed-tokens.md | 67 + docs/configuration/buckets.md | 136 + docs/configuration/credentials.md | 66 + docs/configuration/index.md | 68 + docs/configuration/providers/cached.md | 42 + docs/configuration/providers/dynamodb.md | 33 + docs/configuration/providers/http.md | 39 + docs/configuration/providers/index.md | 57 + docs/configuration/providers/postgres.md | 28 + docs/configuration/providers/static-file.md | 86 + docs/configuration/roles.md | 149 ++ docs/deployment/cloudflare-workers.md | 95 + docs/deployment/index.md | 13 + docs/deployment/server.md | 81 + docs/extending/custom-backend.md | 120 + docs/extending/custom-provider.md | 104 + docs/extending/custom-resolver.md | 128 + docs/extending/index.md | 17 + docs/getting-started/index.md | 63 + docs/getting-started/local-development.md | 105 + docs/guide/authentication.md | 135 + docs/guide/client-usage.md | 109 + docs/guide/index.md | 23 + docs/index.md | 52 + docs/package.json | 20 + docs/pnpm-lock.yaml | 2620 +++++++++++++++++++ docs/reference/config-example.md | 164 ++ docs/reference/errors.md | 58 + docs/reference/index.md | 5 + docs/reference/operations.md | 43 + 42 files changed, 6128 insertions(+) create mode 100644 .claude/launch.json create mode 100644 docs/.vitepress/config.ts create mode 100644 docs/.vitepress/theme/index.ts create mode 100644 docs/.vitepress/theme/style.css create mode 100644 docs/architecture/crate-layout.md create mode 100644 docs/architecture/index.md create mode 100644 docs/architecture/multi-runtime.md create mode 100644 docs/architecture/request-lifecycle.md create mode 100644 docs/auth/backend-auth.md create mode 100644 docs/auth/index.md create mode 100644 docs/auth/proxy-auth.md create mode 100644 docs/auth/sealed-tokens.md create mode 100644 docs/configuration/buckets.md create mode 100644 docs/configuration/credentials.md create mode 100644 docs/configuration/index.md create mode 100644 docs/configuration/providers/cached.md create mode 100644 docs/configuration/providers/dynamodb.md create mode 100644 docs/configuration/providers/http.md create mode 100644 docs/configuration/providers/index.md create mode 100644 docs/configuration/providers/postgres.md create mode 100644 docs/configuration/providers/static-file.md create mode 100644 docs/configuration/roles.md create mode 100644 docs/deployment/cloudflare-workers.md create mode 100644 docs/deployment/index.md create mode 100644 docs/deployment/server.md create mode 100644 docs/extending/custom-backend.md create mode 100644 docs/extending/custom-provider.md create mode 100644 docs/extending/custom-resolver.md create mode 100644 docs/extending/index.md create mode 100644 docs/getting-started/index.md create mode 100644 docs/getting-started/local-development.md create mode 100644 docs/guide/authentication.md create mode 100644 docs/guide/client-usage.md create mode 100644 docs/guide/index.md create mode 100644 docs/index.md create mode 100644 docs/package.json create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/reference/config-example.md create mode 100644 docs/reference/errors.md create mode 100644 docs/reference/index.md create mode 100644 docs/reference/operations.md diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..2d3c45e --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "docs", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["docs:dev"], + "port": 5173, + "cwd": "docs" + } + ] +} diff --git a/.gitignore b/.gitignore index a55e53a..abaabb6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ scripts/task_definition.json target .wrangler .env* +node_modules +docs/.vitepress/cache +docs/.vitepress/dist diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..bf4dfb5 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,165 @@ +import { defineConfig } from "vitepress"; +import { withMermaid } from "vitepress-plugin-mermaid"; + +const adminSidebar = [ + { + text: "Getting Started", + items: [ + { text: "Quick Start", link: "/getting-started/" }, + { + text: "Local Development", + link: "/getting-started/local-development", + }, + ], + }, + { + text: "Configuration", + items: [ + { text: "Overview", link: "/configuration/" }, + { text: "Buckets", link: "/configuration/buckets" }, + { text: "Roles", link: "/configuration/roles" }, + { text: "Credentials", link: "/configuration/credentials" }, + { + text: "Providers", + collapsed: false, + items: [ + { text: "Overview", link: "/configuration/providers/" }, + { + text: "Static File", + link: "/configuration/providers/static-file", + }, + { text: "HTTP API", link: "/configuration/providers/http" }, + { + text: "DynamoDB", + link: "/configuration/providers/dynamodb", + }, + { + text: "PostgreSQL", + link: "/configuration/providers/postgres", + }, + { + text: "Caching", + link: "/configuration/providers/cached", + }, + ], + }, + ], + }, + { + text: "Authentication", + items: [ + { text: "Overview", link: "/auth/" }, + { + text: "Client Auth (OIDC/STS)", + link: "/auth/proxy-auth", + }, + { + text: "Backend Auth", + link: "/auth/backend-auth", + }, + { text: "Sealed Session Tokens", link: "/auth/sealed-tokens" }, + ], + }, + { + text: "Deployment", + items: [ + { text: "Overview", link: "/deployment/" }, + { text: "Server Runtime", link: "/deployment/server" }, + { + text: "Cloudflare Workers", + link: "/deployment/cloudflare-workers", + }, + ], + }, + { + text: "Architecture", + items: [ + { text: "Overview", link: "/architecture/" }, + { text: "Crate Layout", link: "/architecture/crate-layout" }, + { + text: "Request Lifecycle", + link: "/architecture/request-lifecycle", + }, + { + text: "Multi-Runtime Design", + link: "/architecture/multi-runtime", + }, + ], + }, + { + text: "Extending", + items: [ + { text: "Overview", link: "/extending/" }, + { text: "Custom Resolver", link: "/extending/custom-resolver" }, + { text: "Custom Provider", link: "/extending/custom-provider" }, + { text: "Custom Backend", link: "/extending/custom-backend" }, + ], + }, +]; + +export default withMermaid( + defineConfig({ + title: "Source Data Proxy", + description: "Multi-runtime S3 gateway proxy in Rust", + + themeConfig: { + nav: [ + { text: "User Guide", link: "/guide/" }, + { text: "Administration", link: "/getting-started/" }, + { text: "Reference", link: "/reference/" }, + ], + + sidebar: { + "/guide/": [ + { + text: "User Guide", + items: [ + { text: "Overview", link: "/guide/" }, + { text: "Authentication", link: "/guide/authentication" }, + { text: "Client Usage", link: "/guide/client-usage" }, + ], + }, + ], + + "/getting-started/": adminSidebar, + "/configuration/": adminSidebar, + "/auth/": adminSidebar, + "/deployment/": adminSidebar, + "/architecture/": adminSidebar, + "/extending/": adminSidebar, + + "/reference/": [ + { + text: "Reference", + items: [ + { text: "Overview", link: "/reference/" }, + { + text: "Supported Operations", + link: "/reference/operations", + }, + { text: "Error Codes", link: "/reference/errors" }, + { text: "Config Example", link: "/reference/config-example" }, + ], + }, + ], + }, + + socialLinks: [ + { + icon: "github", + link: "https://github.com/source-cooperative/data.source.coop", + }, + ], + + search: { + provider: "local", + }, + + footer: { + message: "Released under the MIT / Apache-2.0 License.", + copyright: + 'A Radiant Earth project. Copyright © 2026 Source Cooperative.', + }, + }, + }), +); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..617deeb --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,4 @@ +import DefaultTheme from "vitepress/theme"; +import "./style.css"; + +export default DefaultTheme; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 0000000..4bd08e0 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,220 @@ +/** + * Source Cooperative theme + * + * Matches docs.source.coop visual identity: + * - Font: IBM Plex Sans + * - Mono: Berkeley Mono (with fallbacks) + * - Light: warm off-white #efebea background, dark teal-gray #2c3233 text + * - Dark: inverted — #2c3233 background, #efebea text + * + * Reference: github.com/source-cooperative/docs.source.coop/blob/main/src/css/custom.css + */ + +/* ------------------------------------------------------------------ */ +/* Typography */ +/* ------------------------------------------------------------------ */ + +@import url("https://fonts.googleapis.com/css2?family=Cascadia+Mono:wght@400;600&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap"); + +:root { + --vp-font-family-base: "IBM Plex Sans", system-ui, -apple-system, sans-serif; + --vp-font-family-mono: "IBM Plex Mono", SFMono-Regular, "SF Mono", Menlo, + Consolas, "Liberation Mono", monospace; + --vp-font-family-heading: "Cascadia Mono", var(--vp-font-family-mono); +} + +/* ------------------------------------------------------------------ */ +/* Light theme colors */ +/* Warm off-white background, dark teal-gray primary */ +/* ------------------------------------------------------------------ */ + +:root { + /* Primary — dark teal-gray (same as docs.source.coop) */ + --vp-c-brand-1: #2c3233; + --vp-c-brand-2: #303738; + --vp-c-brand-3: #394142; + --vp-c-brand-soft: rgba(44, 50, 51, 0.08); + + /* Page background — warm off-white */ + --vp-c-bg: #efebea; + --vp-c-bg-alt: #e6e1df; + --vp-c-bg-elv: #ffffff; + --vp-c-bg-soft: #e6e1df; + + /* Text */ + --vp-c-text-1: #2c3233; + --vp-c-text-2: rgba(44, 50, 51, 0.78); + --vp-c-text-3: rgba(44, 50, 51, 0.56); + + /* Borders & dividers */ + --vp-c-divider: rgba(44, 50, 51, 0.12); + --vp-c-gutter: rgba(44, 50, 51, 0.06); + --vp-c-border: rgba(44, 50, 51, 0.15); + + /* Tip callout — inherit brand */ + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + /* Navbar / sidebar */ + --vp-nav-bg-color: #ffffff; + --vp-sidebar-bg-color: #f7f4f3; + + /* Code blocks */ + --vp-code-block-bg: #f6f7f8; + --vp-code-bg: rgba(44, 50, 51, 0.06); +} + +/* ------------------------------------------------------------------ */ +/* Dark theme colors */ +/* Inverted: teal-gray background, warm off-white primary */ +/* ------------------------------------------------------------------ */ + +.dark { + --vp-c-brand-1: #efebea; + --vp-c-brand-2: #dbd1cf; + --vp-c-brand-3: #b29e99; + --vp-c-brand-soft: rgba(239, 235, 234, 0.1); + + /* Page background */ + --vp-c-bg: #2c3233; + --vp-c-bg-alt: #242a2b; + --vp-c-bg-elv: #343b3c; + --vp-c-bg-soft: #343b3c; + + /* Text */ + --vp-c-text-1: #efebea; + --vp-c-text-2: rgba(239, 235, 234, 0.72); + --vp-c-text-3: rgba(239, 235, 234, 0.48); + + /* Borders & dividers */ + --vp-c-divider: rgba(239, 235, 234, 0.12); + --vp-c-gutter: rgba(239, 235, 234, 0.06); + --vp-c-border: rgba(239, 235, 234, 0.15); + + /* Navbar / sidebar */ + --vp-nav-bg-color: #242a2b; + --vp-sidebar-bg-color: #272e2f; + + /* Code blocks */ + --vp-code-block-bg: rgba(255, 255, 255, 0.06); + --vp-code-bg: rgba(255, 255, 255, 0.1); +} + +/* ------------------------------------------------------------------ */ +/* Buttons */ +/* ------------------------------------------------------------------ */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: #efebea; + --vp-button-brand-bg: #2c3233; + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: #efebea; + --vp-button-brand-hover-bg: #394142; + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: #efebea; + --vp-button-brand-active-bg: #1f2324; +} + +.dark { + --vp-button-brand-text: #2c3233; + --vp-button-brand-bg: #efebea; + --vp-button-brand-hover-text: #2c3233; + --vp-button-brand-hover-bg: #ffffff; + --vp-button-brand-active-text: #2c3233; + --vp-button-brand-active-bg: #dbd1cf; +} + +/* ------------------------------------------------------------------ */ +/* Home hero */ +/* ------------------------------------------------------------------ */ + +:root { + --vp-home-hero-name-color: #2c3233; + --vp-home-hero-name-background: none; + --vp-home-hero-image-background-image: none; + --vp-home-hero-image-filter: none; +} + +.dark { + --vp-home-hero-name-color: #efebea; +} + +/* ------------------------------------------------------------------ */ +/* Headings — Cascadia Mono for a distinctive technical feel */ +/* ------------------------------------------------------------------ */ + +.vp-doc h1, +.vp-doc h2, +.vp-doc h3, +.vp-doc h4, +.vp-doc h5, +.vp-doc h6 { + font-family: var(--vp-font-family-heading); + letter-spacing: -0.02em; +} + +/* Hero heading on homepage */ +.VPHero .name, +.VPHero .text { + font-family: var(--vp-font-family-heading) !important; + letter-spacing: -0.03em; +} + +/* Feature card titles */ +.VPFeature .title { + font-family: var(--vp-font-family-heading); +} + +/* Sidebar group headings */ +.VPSidebarItem.level-0 > .item > .text { + font-family: var(--vp-font-family-heading); +} + +/* ------------------------------------------------------------------ */ +/* Content links — underlined, matching docs.source.coop */ +/* ------------------------------------------------------------------ */ + +.vp-doc a { + text-decoration: underline; + text-underline-offset: 2px; +} + +.vp-doc a:hover { + text-decoration: none; +} + +/* ------------------------------------------------------------------ */ +/* Navbar — clean white surface (light) / dark surface (dark) */ +/* ------------------------------------------------------------------ */ + +.VPNav { + background-color: var(--vp-nav-bg-color) !important; +} + +.VPNavBar { + background-color: var(--vp-nav-bg-color) !important; + border-bottom: 1px solid var(--vp-c-divider) !important; +} + +.VPNavBar .divider { + display: none; +} + +/* ------------------------------------------------------------------ */ +/* Sidebar — subtle background */ +/* ------------------------------------------------------------------ */ + +.VPSidebar { + background-color: var(--vp-sidebar-bg-color) !important; +} + +/* ------------------------------------------------------------------ */ +/* Code font size — match docs.source.coop 95% */ +/* ------------------------------------------------------------------ */ + +:root { + --vp-code-font-size: 95%; +} diff --git a/docs/architecture/crate-layout.md b/docs/architecture/crate-layout.md new file mode 100644 index 0000000..3870f84 --- /dev/null +++ b/docs/architecture/crate-layout.md @@ -0,0 +1,114 @@ +# Crate Layout + +The project is organized as a Cargo workspace with libraries (traits and logic) and runtimes (executable targets). + +``` +crates/ +├── cli/ # source-coop CLI (OIDC login → STS credential exchange) +├── libs/ # Libraries — not directly runnable +│ ├── core/ (source-coop-core) # Runtime-agnostic: traits, S3 parsing, SigV4, config +│ ├── sts/ (source-coop-sts) # OIDC/STS token exchange (AssumeRoleWithWebIdentity) +│ ├── oidc-provider/ # Outbound OIDC provider (JWT signing, JWKS, exchange) +│ └── source-coop/ # Source Cooperative resolver and API client +└── runtimes/ # Runnable targets — one per deployment platform + ├── server/ (source-coop-server) # Tokio/Hyper for container deployments + └── cf-workers/ # Cloudflare Workers for edge deployments +``` + +## Crate Responsibilities + +### `source-coop-core` + +The runtime-agnostic core. Contains: +- `ProxyHandler` — Two-phase request handler (`resolve_request()` → `HandlerAction`) +- `RequestResolver` and `DefaultResolver` — Request parsing, SigV4 auth, authorization +- `ConfigProvider` trait and implementations (static file, HTTP, DynamoDB, Postgres) +- `ProxyBackend` trait — Runtime abstraction for store/signer/raw HTTP +- S3 request parsing, XML response building, list prefix rewriting +- SigV4 signature verification +- Sealed session token encryption/decryption +- Type definitions (`BucketConfig`, `RoleConfig`, `AccessScope`, etc.) + +**Feature flags:** +- `config-http` — HTTP API config provider +- `config-dynamodb` — DynamoDB config provider +- `config-postgres` — PostgreSQL config provider +- `azure` — Azure Blob Storage support +- `gcp` — Google Cloud Storage support + +### `source-coop-sts` + +OIDC token exchange implementing `AssumeRoleWithWebIdentity`: +- JWT decoding and validation (RS256) +- JWKS fetching and caching +- Trust policy evaluation (issuer, audience, subject conditions) +- Temporary credential minting with scope template variables + +### `source-coop-oidc-provider` + +Outbound OIDC identity provider for backend authentication: +- RSA JWT signing (`JwtSigner`) +- JWKS endpoint serving +- OpenID Connect discovery document +- AWS credential exchange (`AwsOidcBackendAuth`) +- Credential caching + +### `source-coop-server` + +The native server runtime: +- Tokio/Hyper HTTP server +- `ServerBackend` implementing `ProxyBackend` with reqwest +- Streaming via hyper `Incoming` bodies and reqwest `bytes_stream()` +- CLI argument parsing (`--config`, `--listen`, `--domain`, `--sts-config`) + +### `source-coop-cf-workers` + +The Cloudflare Workers WASM runtime: +- `WorkerBackend` implementing `ProxyBackend` with `web_sys::fetch` +- `FetchConnector` bridging `object_store` HTTP to Workers Fetch API +- JS `ReadableStream` passthrough for zero-copy streaming +- Config loading from env vars (`PROXY_CONFIG`) + +::: warning +This crate is excluded from the workspace `default-members` because WASM types are `!Send` and won't compile on native targets. Always build with `--target wasm32-unknown-unknown`. +::: + +### `source-coop` (lib) + +Source Cooperative-specific resolver and API client: +- `SourceCoopResolver` — Custom namespace mapping (`/{account}/{repo}/{key}`) +- External auth via Source Cooperative API + +### `cli` + +Command-line tool for OIDC authentication: +- Browser-based OAuth2 Authorization Code + PKCE flow +- `credential_process` integration with AWS SDKs +- Credential caching in `~/.source-coop/credentials/` + +## Dependency Flow + +```mermaid +flowchart TD + core["source-coop-core"] + sts["source-coop-sts"] + oidc["source-coop-oidc-provider"] + api["source-coop (lib)"] + server["source-coop-server"] + workers["source-coop-cf-workers"] + cli["source-coop CLI"] + + server --> core + server --> sts + server --> oidc + workers --> core + workers --> sts + workers --> oidc + workers --> api + cli --> sts + sts --> core + oidc --> core + api --> core +``` + +Libraries define trait abstractions. Runtimes implement `ProxyBackend` with platform-native primitives and wire everything together. diff --git a/docs/architecture/index.md b/docs/architecture/index.md new file mode 100644 index 0000000..652f564 --- /dev/null +++ b/docs/architecture/index.md @@ -0,0 +1,57 @@ +# Architecture Overview + +The Source Data Proxy is an S3-compliant gateway that sits between clients and backend object stores. It provides authentication, authorization, and transparent proxying with zero-copy streaming. + +## High-Level Architecture + +```mermaid +flowchart LR + Clients["S3 Clients\n(aws-cli, boto3, SDKs)"] + + subgraph Proxy["source-coop-proxy"] + Resolver["Request Resolver\n(parse, auth, authorize)"] + Handler["Proxy Handler\n(dispatch operations)"] + Backend["Proxy Backend\n(runtime-specific I/O)"] + end + + Config["Config Provider\n(Static, HTTP, DynamoDB, Postgres)"] + OIDC["OIDC Providers\n(Auth0, GitHub, Keycloak)"] + Stores["Object Stores\n(S3, MinIO, R2, Azure, GCS)"] + + Clients <--> Resolver + Resolver <--> Config + Resolver <--> OIDC + Handler <--> Backend + Backend <--> Stores +``` + +## Design Principles + +**Runtime-agnostic core** — The core proxy logic (`source-coop-core`) has zero runtime dependencies. No Tokio, no `worker-rs`. It compiles to both native and WASM targets. + +**Two-phase handler** — The proxy handler separates request resolution from execution. `resolve_request()` determines what to do; the runtime executes it. This keeps streaming logic in runtime-specific code where it belongs. + +**Presigned URLs for streaming** — GET, HEAD, PUT, and DELETE operations use presigned URLs. The runtime forwards the request directly to the backend — no buffering, no double-handling of bodies. + +**Pluggable traits** — Three trait boundaries enable customization: +- `RequestResolver` — How requests are parsed, authenticated, and authorized +- `ConfigProvider` — Where configuration comes from +- `ProxyBackend` — How the runtime interacts with backends + +## Key Components + +| Component | Crate | Responsibility | +|-----------|-------|---------------| +| [Proxy Handler](./request-lifecycle) | `core` | Dispatch operations via presigned URLs, LIST, or multipart | +| [Request Resolver](./request-lifecycle#request-resolution) | `core` | Parse S3 requests, authenticate, authorize | +| [Config Providers](/configuration/providers/) | `core` | Load buckets, roles, credentials | +| [STS Handler](/auth/proxy-auth#oidcsts-temporary-credentials) | `sts` | OIDC token exchange, credential minting | +| [OIDC Provider](/auth/backend-auth#oidc-backend-auth) | `oidc-provider` | Self-signed JWT minting, backend credential exchange | +| [Server Runtime](./multi-runtime#server-runtime) | `server` | Tokio/Hyper HTTP server | +| [Workers Runtime](./multi-runtime#cloudflare-workers-runtime) | `cf-workers` | WASM-based Cloudflare Workers | + +## Further Reading + +- [Crate Layout](./crate-layout) — How the workspace is organized +- [Request Lifecycle](./request-lifecycle) — How a request flows through the proxy +- [Multi-Runtime Design](./multi-runtime) — How the same core runs on native and WASM diff --git a/docs/architecture/multi-runtime.md b/docs/architecture/multi-runtime.md new file mode 100644 index 0000000..94e6912 --- /dev/null +++ b/docs/architecture/multi-runtime.md @@ -0,0 +1,78 @@ +# Multi-Runtime Design + +The proxy runs on two runtimes — a native Tokio/Hyper server for container deployments and Cloudflare Workers for edge deployments. The same core logic compiles to both targets through careful abstraction of platform-specific concerns. + +## Runtime Comparison + +| | Server Runtime | CF Workers Runtime | +|---|---|---| +| **Platform** | Linux/macOS containers | Cloudflare Workers (V8) | +| **Target** | `x86_64` / `aarch64` | `wasm32-unknown-unknown` | +| **HTTP client** | reqwest | `web_sys::fetch` | +| **Streaming** | hyper `Incoming` / reqwest `bytes_stream()` | JS `ReadableStream` passthrough | +| **Object store connector** | Default (reqwest-based) | `FetchConnector` | +| **Backend support** | S3, Azure, GCS | S3 only | +| **Config loading** | TOML file | Env var (JSON or JS object) | +| **Threading** | Multi-threaded (`Send + Sync` required) | Single-threaded (`!Send` types allowed) | + +## How It Works + +### MaybeSend / MaybeSync + +The core challenge is that Tokio requires `Send + Sync` for task spawning, while WASM runtimes are single-threaded and use `!Send` types (like `JsValue` and `ReadableStream`). + +The solution is conditional trait aliases defined in `source-coop-core`: + +- On native targets: `MaybeSend` resolves to `Send`, `MaybeSync` resolves to `Sync` +- On `wasm32`: `MaybeSend` and `MaybeSync` are blanket traits that every type implements + +All core traits (`ProxyBackend`, `RequestResolver`, `ConfigProvider`) use `MaybeSend + MaybeSync` instead of `Send + Sync`, so they compile on both targets. + +The `Signer` trait from `object_store` requires real `Send + Sync`, which works because `UnsignedUrlSigner` only holds `String` fields, and `object_store`'s built-in store types are `Send + Sync`. + +### RPITIT Async Methods + +Core traits use return-position `impl Trait` in trait (RPITIT) for async methods instead of `#[async_trait]`: + +```rust +pub trait RequestResolver: Clone + MaybeSend + MaybeSync + 'static { + fn resolve( + &self, + method: &Method, + path: &str, + query: Option<&str>, + headers: &HeaderMap, + ) -> impl Future> + MaybeSend; +} +``` + +This avoids `#[async_trait]`'s `Box` requirement, which won't compile on WASM targets. + +## Server Runtime + +The server runtime (`crates/runtimes/server/`) uses Tokio and Hyper: + +- **Forward actions**: reqwest sends the presigned URL request. For GET, the response body is streamed via `bytes_stream()`. For PUT, the client's hyper `Incoming` body is streamed directly to reqwest. +- **`ServerBackend`**: Creates `object_store` instances with the default HTTP connector (reqwest) and uses reqwest for `send_raw()` (multipart). + +## Cloudflare Workers Runtime + +The CF Workers runtime (`crates/runtimes/cf-workers/`) uses `worker-rs`, `wasm-bindgen`, and `web_sys`: + +- **Forward actions**: JS `ReadableStream` bodies pass through without touching Rust. The Workers Fetch API handles streaming natively. +- **`WorkerBackend`**: Creates `object_store` instances with `FetchConnector` injected for HTTP transport. + +### FetchConnector + +`FetchConnector` bridges `object_store`'s `HttpConnector` trait to the Workers Fetch API. Since `worker::Fetch::send()` is `!Send`, each call is wrapped in `spawn_local` with a oneshot channel to bridge back to the `Send` context that `object_store` expects. + +This is only used for LIST operations — presigned URL operations bypass `object_store` entirely. + +### WASM Limitations + +- **S3 only**: Azure and GCS builders are gated behind cargo features that are disabled for the Workers runtime +- **`Instant::now()` panics on WASM**: The `UnsignedUrlSigner` avoids the `InstanceCredentialProvider` → `TokenCache` → `Instant::now()` code path that panics on WASM +- **No `default-members`**: The CF Workers crate is excluded from the workspace default members. Always build with: + ```bash + cargo check -p source-coop-cf-workers --target wasm32-unknown-unknown + ``` diff --git a/docs/architecture/request-lifecycle.md b/docs/architecture/request-lifecycle.md new file mode 100644 index 0000000..05556f8 --- /dev/null +++ b/docs/architecture/request-lifecycle.md @@ -0,0 +1,95 @@ +# Request Lifecycle + +Every S3 request flows through a two-phase dispatch model: first the request is resolved (parsed, authenticated, authorized), then the appropriate action is executed by the runtime. + +## Overview + +```mermaid +sequenceDiagram + participant Client + participant Runtime as Runtime
(Server or Workers) + participant Resolver as Request Resolver + participant Handler as Proxy Handler + participant Backend as Backend Store + + Client->>Runtime: HTTP request + Runtime->>Handler: resolve_request(method, path, query, headers) + Handler->>Resolver: resolve(method, path, query, headers) + Resolver->>Resolver: Parse S3 operation + Resolver->>Resolver: Authenticate (SigV4) + Resolver->>Resolver: Authorize (check scopes) + Resolver-->>Handler: ResolvedAction::Proxy + Handler->>Handler: Dispatch operation + Handler-->>Runtime: HandlerAction + + alt Forward (GET/HEAD/PUT/DELETE) + Runtime->>Backend: Execute presigned URL + Backend-->>Runtime: Stream response + Runtime-->>Client: Stream response + else Response (LIST, errors) + Runtime-->>Client: Return response body + else NeedsBody (multipart) + Runtime->>Runtime: Collect request body + Runtime->>Handler: handle_with_body(pending, body) + Handler->>Backend: Signed multipart request + Backend-->>Handler: Response + Handler-->>Runtime: ProxyResult + Runtime-->>Client: Response + end +``` + +## Phase 1: Request Resolution + +The `RequestResolver` determines what to do with an incoming request. The `DefaultResolver` handles standard S3 proxy behavior: + +1. **Parse the S3 operation** from the HTTP method, path, query, and headers + - Path-style: `GET /bucket/key` → GetObject on `bucket` with key `key` + - Virtual-hosted: `GET /key` with `Host: bucket.s3.example.com` → same operation +2. **Authenticate** the request by verifying the SigV4 signature against stored or sealed credentials +3. **Authorize** by checking the caller's access scopes against the requested bucket, key prefix, and operation +4. **Return** a `ResolvedAction`: + - `Proxy { operation, bucket_config, list_rewrite }` — forward to a backend + - `Response { status, headers, body }` — return a synthetic response (e.g., `ListBuckets`) + +Custom resolvers can implement entirely different routing, authentication, and namespace mapping. + +## Phase 2: Handler Dispatch + +The `ProxyHandler` takes the resolved action and dispatches it based on the S3 operation type. It returns a `HandlerAction` enum: + +### `Forward(ForwardRequest)` + +Used for: **GET, HEAD, PUT, DELETE** + +The handler generates a presigned URL using the backend's `Signer` and returns it to the runtime with filtered headers. The runtime executes the presigned URL with its native HTTP client, streaming request and response bodies directly. The handler never touches the body data. + +- Presigned URL TTL: 300 seconds +- Headers forwarded: `range`, `if-match`, `if-none-match`, `if-modified-since`, `if-unmodified-since`, `content-type`, `content-length`, `content-md5`, `content-encoding`, `content-disposition`, `cache-control`, `x-amz-content-sha256` + +### `Response(ProxyResult)` + +Used for: **LIST, errors, synthetic responses** + +For LIST operations, the handler calls `object_store::list_with_delimiter()` via the backend's store, builds S3 `ListObjectsV2` XML from the results, and returns it as a complete response. If a `ListRewrite` is configured, key prefixes are transformed in the XML. + +::: info +LIST returns all results in a single response. `IsTruncated` is always `false`. The proxy does not support S3-style pagination with continuation tokens. +::: + +### `NeedsBody(PendingRequest)` + +Used for: **CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload** + +Multipart operations need the request body (e.g., the XML body for `CompleteMultipartUpload`). The runtime materializes the body, then calls `handler.handle_with_body()`, which signs the request using `S3RequestSigner` and sends it via `backend.send_raw()`. + +::: warning +Multipart uploads are only supported for `backend_type = "s3"`. Non-S3 backends should use single PUT requests (object_store handles chunking internally). +::: + +## Response Header Forwarding + +The proxy forwards only specific headers from the backend response to the client: + +`content-type`, `content-length`, `content-range`, `etag`, `last-modified`, `accept-ranges`, `content-encoding`, `content-disposition`, `cache-control`, `x-amz-request-id`, `x-amz-version-id`, `location` + +All other backend headers are filtered out. diff --git a/docs/auth/backend-auth.md b/docs/auth/backend-auth.md new file mode 100644 index 0000000..d6e68ce --- /dev/null +++ b/docs/auth/backend-auth.md @@ -0,0 +1,255 @@ +# Authenticating with Object Store Backends + +The proxy needs credentials to access backend object stores (S3, Azure Blob Storage, GCS). There are two approaches: static credentials stored in the proxy config, and OIDC-based credential resolution where the proxy acts as its own identity provider. + +## Static Backend Credentials + +The simplest approach is to include credentials directly in the bucket's `backend_options`: + +```toml +[[buckets]] +name = "my-data" +backend_type = "s3" + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" +access_key_id = "AKIAIOSFODNN7EXAMPLE" +secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +``` + +This works for any backend type. For anonymous backend access (e.g., public buckets), omit the credential fields and add `skip_signature = "true"`. + +## OIDC Backend Auth + +For production deployments, the proxy can act as its own OIDC identity provider. Instead of storing long-lived backend credentials, the proxy mints self-signed JWTs and exchanges them with cloud providers for temporary credentials — the same pattern used by GitHub Actions and Vercel for AWS access. + +### How It Works + +```mermaid +sequenceDiagram + participant Client as S3 Client + participant Proxy as Data Proxy + participant Cloud as Cloud Provider STS
(e.g., AWS STS) + participant Store as Object Store
(e.g., S3) + + Client->>Proxy: 1. S3 request (e.g., GET /bucket/key) + Proxy->>Proxy: 2. Authenticate client + Proxy->>Proxy: 3. Bucket has auth_type=oidc
Mint self-signed JWT + Proxy->>Cloud: 4. AssumeRoleWithWebIdentity
(JWT + Role ARN) + Cloud->>Proxy: 5. Fetch /.well-known/jwks.json + Proxy-->>Cloud: 6. RSA public key + Cloud-->>Proxy: 7. Temporary credentials + Proxy->>Proxy: 8. Cache credentials + Proxy->>Store: 9. Forward request with
temporary credentials + Store-->>Proxy: 10. Response + Proxy-->>Client: 11. Response +``` + +### Configuration + +OIDC backend auth requires two environment variables: + +| Variable | Description | +|----------|-------------| +| `OIDC_PROVIDER_KEY` | PEM-encoded RSA private key for JWT signing | +| `OIDC_PROVIDER_ISSUER` | Publicly reachable URL (e.g., `https://data.source.coop`) | + +Generate an RSA key pair: + +```bash +openssl genrsa -out oidc-key.pem 2048 +``` + +Set the environment variables: + +```bash +export OIDC_PROVIDER_KEY=$(cat oidc-key.pem) +export OIDC_PROVIDER_ISSUER="https://data.source.coop" +``` + +Then configure buckets to use OIDC: + +```toml +[[buckets]] +name = "my-data" +backend_type = "s3" + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" +auth_type = "oidc" +oidc_role_arn = "arn:aws:iam::123456789012:role/DataProxyAccess" +``` + +### Discovery Endpoints + +When OIDC provider keys are configured, the proxy serves two well-known endpoints that cloud providers use to validate JWTs: + +**`GET /.well-known/openid-configuration`** +```json +{ + "issuer": "https://data.source.coop", + "jwks_uri": "https://data.source.coop/.well-known/jwks.json", + "response_types_supported": ["id_token"], + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"] +} +``` + +**`GET /.well-known/jwks.json`** +```json +{ + "keys": [{ + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "proxy-key-1", + "n": "", + "e": "" + }] +} +``` + +::: warning +These endpoints must be publicly accessible. Cloud providers fetch them at JWT validation time to verify signatures. If they are behind a firewall or VPN, credential exchange will fail. +::: + +### The Exchange Flow in Detail + +When a request arrives for a bucket with `auth_type=oidc`: + +1. The `OidcBackendAuth` handler detects `auth_type=oidc` in the bucket's `backend_options` +2. It mints a short-lived JWT signed with the proxy's RSA private key: + - `iss`: the configured `OIDC_PROVIDER_ISSUER` + - `sub`: a connection identifier (from `oidc_subject` option, or a default) + - `aud`: the cloud provider's STS audience (e.g., `sts.amazonaws.com`) + - `exp`: short expiration (minutes) +3. The proxy sends the JWT to the cloud provider's STS endpoint along with the target IAM role ARN +4. The cloud provider fetches the proxy's JWKS, verifies the JWT signature, evaluates the role's trust policy, and returns temporary credentials +5. The proxy caches the credentials (keyed by role ARN) and injects them into the bucket config +6. The existing `build_object_store()` / `build_signer()` pipeline consumes the credentials normally + +On subsequent requests, cached credentials are reused until they expire. + +## Cloud Provider Setup + +### AWS S3 + +**Administrator setup:** + +1. **Register the OIDC provider** in your AWS account: + ```bash + aws iam create-open-id-connect-provider \ + --url https://data.source.coop \ + --client-id-list sts.amazonaws.com \ + --thumbprint-list + ``` + + ::: tip + To get the thumbprint, fetch the TLS certificate chain from your proxy's domain. AWS uses this to verify the HTTPS connection to the JWKS endpoint. + ::: + +2. **Create an IAM Role** with a trust policy that allows the proxy to assume it: + ```json + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::123456789012:oidc-provider/data.source.coop" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "data.source.coop:aud": "sts.amazonaws.com", + "data.source.coop:sub": "s3-proxy" + } + } + }] + } + ``` + +3. **Attach an S3 permission policy** to the role: + ```json + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket", + "s3:DeleteObject" + ], + "Resource": [ + "arn:aws:s3:::my-backend-bucket", + "arn:aws:s3:::my-backend-bucket/*" + ] + }] + } + ``` + +4. **Configure the bucket** in the proxy: + ```toml + [[buckets]] + name = "my-data" + backend_type = "s3" + + [buckets.backend_options] + endpoint = "https://s3.us-east-1.amazonaws.com" + bucket_name = "my-backend-bucket" + region = "us-east-1" + auth_type = "oidc" + oidc_role_arn = "arn:aws:iam::123456789012:role/DataProxyAccess" + ``` + +**At request time**, the proxy calls AWS STS `AssumeRoleWithWebIdentity` with the self-signed JWT. No AWS credentials are stored in the proxy configuration. + +### Azure Blob Storage + +::: info Planned +Azure OIDC backend auth is planned but not yet implemented. The proxy currently supports Azure with static credentials only. +::: + +**Planned setup:** + +1. Create an App Registration in Microsoft Entra ID +2. Add a Federated Identity Credential specifying the proxy's issuer URL and expected `sub` claim +3. Grant the app `Storage Blob Data Contributor` on the target storage account +4. The proxy would exchange its JWT for an Azure AD token via `client_credentials` grant with `jwt-bearer` assertion + +### Google Cloud Storage + +::: info Planned +GCS OIDC backend auth is planned but not yet implemented. The proxy currently supports GCS with static credentials only. +::: + +**Planned setup:** + +1. Create a Workload Identity Pool and OIDC Provider, specifying the proxy's issuer URL +2. Map the external identity to a GCP Service Account +3. Grant the service account GCS permissions +4. The proxy would use a two-step exchange: GCP STS token exchange, then `generateAccessToken` to impersonate the service account + +## Credential Caching + +When using OIDC backend auth, the proxy caches temporary credentials to avoid calling the cloud provider's STS on every request. Credentials are: + +- Keyed by the IAM role ARN +- Automatically refreshed when they expire +- Shared across concurrent requests to the same bucket + +This means the first request to an OIDC-backed bucket incurs a small latency cost for the credential exchange, but subsequent requests use cached credentials until they expire. + +## Choosing Between Static and OIDC + +| | Static Credentials | OIDC Backend Auth | +|---|---|---| +| **Setup complexity** | Low | Medium (IAM role + OIDC provider registration) | +| **Credential rotation** | Manual | Automatic (temporary credentials) | +| **Security** | Long-lived secrets in config | No long-lived secrets | +| **Cloud providers** | All (S3, Azure, GCS) | AWS S3 (Azure and GCS planned) | +| **Latency** | None | Small cost on first request (then cached) | diff --git a/docs/auth/index.md b/docs/auth/index.md new file mode 100644 index 0000000..c68523f --- /dev/null +++ b/docs/auth/index.md @@ -0,0 +1,46 @@ +# Authentication + +The Source Data Proxy has two distinct authentication concerns: + +1. **Client authentication** — How clients prove their identity to the proxy +2. **Backend authentication** — How the proxy authenticates with backend object stores + +```mermaid +flowchart LR + Client["S3 Client"] + Proxy["Data Proxy"] + Backend["Object Store\n(S3, Azure, GCS)"] + + Client -- "SigV4, STS/OIDC" --> Proxy + Proxy -- "Presigned URLs,\nOIDC Exchange,\nor Static Credentials" --> Backend +``` + +## Client Authentication + +Clients authenticate with the proxy using one of three methods: + +| Method | Use Case | How It Works | +|--------|----------|--------------| +| **Anonymous** | Public datasets | No credentials needed for GET/HEAD/LIST | +| **Long-lived access keys** | Service accounts, internal tools | Static `AccessKeyId`/`SecretAccessKey` with SigV4 signing | +| **OIDC/STS temporary credentials** | CI/CD, user sessions, federated identity | Exchange a JWT from an OIDC provider for scoped temporary credentials | + +The proxy verifies all signed requests using standard AWS Signature Version 4 (SigV4). Any S3-compatible client works without modification — just set the endpoint URL. + +The OIDC/STS flow is the recommended approach for most use cases. See [Client Auth Setup](./proxy-auth) for configuration details. + +## Backend Authentication + +The proxy authenticates with backend object stores using one of two methods: + +| Method | Use Case | How It Works | +|--------|----------|--------------| +| **Static credentials** | Simple setups | `access_key_id`/`secret_access_key` stored in the proxy config | +| **OIDC backend auth** | Production, credential-free | Proxy acts as its own OIDC provider, exchanges self-signed JWTs for cloud credentials | + +OIDC backend auth eliminates the need to store long-lived backend credentials. See [Backend Auth](./backend-auth) for details. + +## Related Topics + +- [Sealed Session Tokens](./sealed-tokens) — How temporary credentials are encrypted for stateless runtimes +- [User Guide: Authentication](/guide/authentication) — User-facing guide for obtaining credentials and using the CLI diff --git a/docs/auth/proxy-auth.md b/docs/auth/proxy-auth.md new file mode 100644 index 0000000..85c9ca1 --- /dev/null +++ b/docs/auth/proxy-auth.md @@ -0,0 +1,353 @@ +# Client Authentication Setup + +This page covers how to configure the proxy to authenticate incoming client requests. For the user-facing guide on obtaining credentials and using the CLI, see the [User Guide: Authentication](/guide/authentication). + +## Authentication Modes + +The proxy supports three authentication modes: + +| Mode | Config | Use Case | +|------|--------|----------| +| **Anonymous** | `anonymous_access = true` on a bucket | Public datasets, open data | +| **Long-lived access keys** | `[[credentials]]` entries | Service accounts, internal tools | +| **OIDC/STS temporary credentials** | `[[roles]]` with trust policies | CI/CD, user sessions, federated identity | + +## Anonymous Access + +Enable per-bucket: + +```toml +[[buckets]] +name = "public-data" +backend_type = "s3" +anonymous_access = true +``` + +Anonymous access only allows `GetObject`, `HeadObject`, and `ListBucket`. Write operations always require authentication. + +## Long-Lived Access Keys + +Static credentials are defined in the config. Each has an access key pair and scoped permissions: + +```toml +[[credentials]] +access_key_id = "AKPROXY00000EXAMPLE" +secret_access_key = "proxy/secret/key/EXAMPLE000000000000" +principal_name = "internal-dashboard" +created_at = "2024-01-15T00:00:00Z" +enabled = true + +[[credentials.allowed_scopes]] +bucket = "ml-artifacts" +prefixes = ["models/production/"] +actions = ["get_object", "head_object"] +``` + +Clients sign requests using standard AWS SigV4. Any S3-compatible client works without modification. + +## OIDC/STS Temporary Credentials + +This is the recommended authentication method. Clients exchange a JWT from an OIDC-compatible identity provider for scoped, time-limited credentials via `AssumeRoleWithWebIdentity`. + +### How It Works + +```mermaid +sequenceDiagram + participant Client + participant OIDC as OIDC Provider
(Auth0, GitHub, etc.) + participant Proxy as Data Proxy + participant JWKS as Provider JWKS + + Client->>OIDC: 1. Authenticate + OIDC-->>Client: 2. JWT (id_token) + Client->>Proxy: 3. AssumeRoleWithWebIdentity
(JWT + RoleArn) + Proxy->>JWKS: 4. Fetch JWKS (cached) + JWKS-->>Proxy: 5. Public keys + Proxy->>Proxy: 6. Verify JWT signature (RS256) + Proxy->>Proxy: 7. Check trust policy
(issuer, audience, subject) + Proxy->>Proxy: 8. Mint temporary credentials
(seal into session token) + Proxy-->>Client: 9. AccessKeyId + SecretAccessKey
+ SessionToken + Expiration + Client->>Proxy: 10. S3 request with SigV4
(using temporary credentials) +``` + +### Verification Flow + +When a client calls `AssumeRoleWithWebIdentity`: + +1. The proxy decodes the JWT header to extract the `iss` (issuer) and `kid` (key ID) +2. The proxy verifies the issuer is trusted by the requested role +3. The proxy fetches the issuer's JWKS endpoint and verifies the JWT signature (RS256) +4. The proxy evaluates the trust policy: + - **Issuer**: must be in the role's `trusted_oidc_issuers` + - **Audience**: if `required_audience` is set on the role, the token's `aud` claim must match + - **Subject**: the token's `sub` claim must match at least one of the role's `subject_conditions` (supports `*` glob wildcards) +5. The proxy mints temporary credentials scoped to the role's `allowed_scopes` +6. If `SESSION_TOKEN_KEY` is configured, the credentials are AES-256-GCM encrypted into the session token (see [Sealed Session Tokens](./sealed-tokens)) +7. The proxy returns the credentials in an XML response matching the AWS STS format + +### STS Request Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `Action` | Yes | Must be `AssumeRoleWithWebIdentity` | +| `RoleArn` | Yes | The `role_id` of the role to assume | +| `WebIdentityToken` | Yes | The JWT from the OIDC provider | +| `DurationSeconds` | No | Session duration (900s minimum, capped by `max_session_duration_secs`) | + +### STS Response + +The response follows the standard AWS STS XML format: + +```xml + + + + STSPRXY... + ... + ... + 2024-01-15T01:00:00Z + + + github-actions-deployer/alice + github-actions-deployer + + + +``` + +## Integrating with OIDC Providers + +The proxy works with any OIDC-compliant identity provider that serves a JWKS endpoint and issues RS256-signed JWTs. You need: + +1. The provider's issuer URL (must serve `/.well-known/openid-configuration` with a `jwks_uri`) +2. The `sub` claim format for configuring `subject_conditions` +3. Optionally, the audience claim value for `required_audience` + +
+GitHub Actions — OIDC tokens for CI/CD workflows + +#### Role Configuration + +```toml +[[roles]] +role_id = "github-actions-deployer" +name = "GitHub Actions Deploy Role" +trusted_oidc_issuers = ["https://token.actions.githubusercontent.com"] +required_audience = "sts.s3proxy.example.com" +subject_conditions = [ + "repo:myorg/myapp:ref:refs/heads/main", + "repo:myorg/myapp:ref:refs/heads/release/*", +] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "deploy-bundles" +prefixes = [] +actions = ["get_object", "head_object", "put_object"] +``` + +#### Workflow Example + +```yaml +jobs: + deploy: + permissions: + id-token: write # Required for OIDC token + steps: + - name: Get OIDC token + id: oidc + run: | + TOKEN=$(curl -s \ + -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.s3proxy.example.com" \ + | jq -r '.value') + echo "token=$TOKEN" >> $GITHUB_OUTPUT + + - name: Assume role via STS + run: | + CREDS=$(aws sts assume-role-with-web-identity \ + --role-arn github-actions-deployer \ + --web-identity-token ${{ steps.oidc.outputs.token }} \ + --endpoint-url https://s3proxy.example.com \ + --output json) + + echo "AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId')" >> $GITHUB_ENV + echo "AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')" >> $GITHUB_ENV + echo "AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken')" >> $GITHUB_ENV + + - name: Upload to proxy + run: | + aws s3 cp ./bundle.tar.gz s3://deploy-bundles/releases/v1.2.3.tar.gz \ + --endpoint-url https://s3proxy.example.com +``` + +#### Key Details + +- **Issuer URL**: `https://token.actions.githubusercontent.com` +- **Subject format**: `repo:{owner}/{repo}:ref:{ref}` (e.g., `repo:myorg/myapp:ref:refs/heads/main`) +- **Audience**: configurable via the `&audience=` parameter in the token request URL +- The `id-token: write` permission is required in the workflow + +
+ +
+Auth0 — OAuth2/OIDC identity platform + +#### Auth0 Setup + +1. Create an Application (Regular Web Application or SPA) in your Auth0 dashboard +2. Note your Auth0 domain — this is the issuer URL + +#### Role Configuration + +```toml +[[roles]] +role_id = "auth0-user" +name = "Auth0 User" +trusted_oidc_issuers = ["https://your-tenant.auth0.com/"] +required_audience = "https://s3proxy.example.com" +subject_conditions = ["*"] # Or restrict by user ID patterns +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +#### Key Details + +- **Issuer URL**: `https://your-tenant.auth0.com/` (trailing slash required) +- **Subject claim**: Auth0 user ID (e.g., `auth0|507f1f77bcf86cd799439011`) +- **Audience**: set when requesting the token via the `audience` parameter + +
+ +
+Keycloak — Open-source identity and access management + +#### Keycloak Setup + +1. Create a Realm and a Client in your Keycloak admin console +2. Set the client's Access Type to `public` or `confidential` as needed +3. Enable "Standard Flow" (Authorization Code) + +#### Role Configuration + +```toml +[[roles]] +role_id = "keycloak-user" +name = "Keycloak User" +trusted_oidc_issuers = ["https://keycloak.example.com/realms/myrealm"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +#### Key Details + +- **Issuer URL**: `https://keycloak.example.com/realms/{realm-name}` +- **Subject claim**: Keycloak user UUID +- **JWKS**: served at `{issuer}/protocol/openid-connect/certs` + +
+ +
+AWS Cognito — AWS-managed identity service + +#### Cognito Setup + +1. Create a User Pool in the AWS Cognito console +2. Create an App Client (no client secret for public clients) +3. Configure the Hosted UI or use the Cognito SDK for authentication + +#### Role Configuration + +```toml +[[roles]] +role_id = "cognito-user" +name = "Cognito User" +trusted_oidc_issuers = ["https://cognito-idp.us-east-1.amazonaws.com/us-east-1_EXAMPLE"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +#### Key Details + +- **Issuer URL**: `https://cognito-idp.{region}.amazonaws.com/{user-pool-id}` +- **Subject claim**: Cognito user UUID +- **Audience**: the App Client ID (set `required_audience` to match) + +
+ +
+Ory / Ory Network — Open-source OAuth2/OIDC infrastructure + +#### Ory Setup + +1. Create an OAuth2 client as a **public client** (no client secret) +2. Set the grant type to Authorization Code with PKCE +3. Register `http://127.0.0.1/callback` as a redirect URI (any port is allowed per RFC 8252) +4. Set allowed scopes to include `openid` + +#### Role Configuration + +```toml +[[roles]] +role_id = "ory-user" +name = "Ory User" +trusted_oidc_issuers = ["https://your-project.projects.oryapis.com"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +#### Key Details + +- **Issuer URL**: `https://your-project.projects.oryapis.com` (Ory Network) or your self-hosted Hydra URL +- **Subject claim**: Ory identity UUID +- The CLI (`source-coop login`) works well with Ory since Ory follows RFC 8252 for loopback redirect URIs + +
+ +## Template Variables in Scopes + +Role scopes support `{claim_name}` template variables that are resolved from the authenticated user's JWT claims when credentials are minted. This enables per-user access without creating a separate role for each user. + +```toml +[[roles]] +role_id = "source-coop-user" +trusted_oidc_issuers = ["https://auth.source.coop"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +# Each user gets access to a bucket matching their OIDC subject +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +A user with `sub = "alice"` receives credentials scoped to `bucket = "alice"`. Any string claim from the JWT can be referenced — `{email}`, `{org}`, etc. Missing or non-string claims resolve to an empty string, which safely fails authorization. + +You can also use template variables in prefixes for more granular access: + +```toml +[[roles.allowed_scopes]] +bucket = "shared-data" +prefixes = ["{org}/"] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` diff --git a/docs/auth/sealed-tokens.md b/docs/auth/sealed-tokens.md new file mode 100644 index 0000000..2b35ba7 --- /dev/null +++ b/docs/auth/sealed-tokens.md @@ -0,0 +1,67 @@ +# Sealed Session Tokens + +When the proxy mints temporary credentials via STS, it needs a way to recognize those credentials on subsequent requests. Sealed session tokens solve this by encrypting the full credential set into the session token itself — no server-side storage required. + +## Why Sealed Tokens? + +Traditional credential stores keep a mapping from access key ID to credentials on the server. This requires either a database or in-memory state, which is impractical for stateless runtimes like Cloudflare Workers. + +Sealed tokens take a different approach: the credentials are encrypted and placed directly inside the session token that the client sends with every request. The proxy decrypts the token on each request to recover the credentials. + +## How It Works + +### Minting (seal) + +When `AssumeRoleWithWebIdentity` mints temporary credentials: + +1. The full `TemporaryCredentials` struct is serialized to JSON +2. A random 12-byte nonce is generated +3. The JSON is encrypted using AES-256-GCM with the nonce +4. The result is encoded as `base64url(nonce[12] || ciphertext + tag)` +5. This encoded string becomes the `SessionToken` returned to the client + +### Verifying (unseal) + +When a request arrives with an `x-amz-security-token` header: + +1. The proxy base64url-decodes the session token +2. It extracts the nonce (first 12 bytes) and ciphertext (remainder) +3. It decrypts using AES-256-GCM with the configured key +4. The JSON is deserialized back to `TemporaryCredentials` +5. The proxy checks that the credentials haven't expired +6. The proxy verifies the request's SigV4 signature against the decrypted secret key + +If the token doesn't look like a sealed token (e.g., not valid base64url), the proxy falls back to looking up credentials from the config provider. + +## Configuration + +Set the `SESSION_TOKEN_KEY` environment variable to a base64-encoded 32-byte key: + +```bash +# Generate a key +openssl rand -base64 32 + +# Set it +export SESSION_TOKEN_KEY="" +``` + +This key must be the same across all instances of the proxy. If you rotate the key, all existing session tokens become invalid — clients will need to re-authenticate. + +::: warning +`SESSION_TOKEN_KEY` is required for the Cloudflare Workers runtime. Without it, temporary credentials from STS cannot be verified on subsequent requests. +::: + +## Scope Behavior + +Access scopes are sealed into the token at mint time. This means: + +- Changing a role's `allowed_scopes` in the config only affects newly minted credentials +- Existing session tokens continue to use the scopes they were minted with until they expire +- There is no way to revoke a sealed token short of rotating the encryption key (which invalidates all tokens) + +## Security Properties + +- **Confidentiality**: AES-256-GCM encryption prevents clients from reading or modifying the sealed credentials +- **Integrity**: The GCM authentication tag detects any tampering with the ciphertext +- **Replay protection**: Each token has a random nonce; however, tokens are valid until their expiration time +- **Constant-time comparison**: The access key ID verification uses constant-time comparison to prevent timing attacks diff --git a/docs/configuration/buckets.md b/docs/configuration/buckets.md new file mode 100644 index 0000000..86edd21 --- /dev/null +++ b/docs/configuration/buckets.md @@ -0,0 +1,136 @@ +# Buckets + +Buckets define the virtual namespaces that clients interact with. Each bucket maps a client-visible name to a backend object store. + +## Configuration + +```toml +[[buckets]] +name = "my-data" +backend_type = "s3" +backend_prefix = "v2" +anonymous_access = false +allowed_roles = ["github-actions-deployer"] + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" +access_key_id = "AKIAIOSFODNN7EXAMPLE" +secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +``` + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Client-visible bucket name | +| `backend_type` | string | Yes | Backend provider: `"s3"`, `"az"`, or `"gcs"` | +| `backend_prefix` | string | No | Prefix prepended to keys when forwarding to the backend | +| `anonymous_access` | bool | No | Allow GET/HEAD/LIST without authentication (default: `false`) | +| `allowed_roles` | string[] | No | Role IDs that can be assumed for this bucket | +| `backend_options` | map | Yes | Provider-specific configuration (see below) | + +## Backend Options by Provider + +### S3 / MinIO / R2 + +```toml +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" +access_key_id = "AKIA..." +secret_access_key = "..." +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `endpoint` | Yes | S3 endpoint URL | +| `bucket_name` | Yes | Backend bucket name | +| `region` | Yes | AWS region | +| `access_key_id` | No | AWS access key (omit for anonymous or OIDC) | +| `secret_access_key` | No | AWS secret key | +| `skip_signature` | No | Set to `"true"` for unsigned requests | + +### Azure Blob Storage + +::: info +Requires the `azure` feature flag on `source-coop-core`. Enabled by default in the server runtime, not available in CF Workers. +::: + +```toml +[buckets.backend_options] +account_name = "mystorageaccount" +container_name = "my-container" +access_key = "..." +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `account_name` | Yes | Azure storage account name | +| `container_name` | Yes | Blob container name | +| `access_key` | No | Storage account access key | +| `skip_signature` | No | Set to `"true"` for anonymous access | + +### Google Cloud Storage + +::: info +Requires the `gcp` feature flag on `source-coop-core`. Enabled by default in the server runtime, not available in CF Workers. +::: + +```toml +[buckets.backend_options] +bucket_name = "my-gcs-bucket" +service_account_key = '{"type": "service_account", ...}' +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `bucket_name` | Yes | GCS bucket name | +| `service_account_key` | No | JSON service account key | +| `skip_signature` | No | Set to `"true"` for anonymous access | + +### OIDC Backend Auth Options + +For any backend type, you can use OIDC-based credential resolution instead of static credentials: + +```toml +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" +auth_type = "oidc" +oidc_role_arn = "arn:aws:iam::123456789012:role/ProxyRole" +oidc_subject = "my-connection" # optional, defaults to "s3-proxy" +``` + +See [Authenticating with Backends](/auth/backend-auth) for setup details. + +## Backend Prefix + +The `backend_prefix` field transparently prepends a path prefix to all keys when forwarding requests to the backend. Clients don't see this prefix. + +```toml +[[buckets]] +name = "ml-artifacts" +backend_prefix = "v2" + +[buckets.backend_options] +bucket_name = "ml-pipeline-artifacts" +``` + +With this configuration: +- Client requests `GET /ml-artifacts/models/latest.pt` +- Proxy forwards to backend key `v2/models/latest.pt` in `ml-pipeline-artifacts` +- LIST responses have the prefix stripped so clients see `models/latest.pt` + +## Anonymous Access + +Setting `anonymous_access = true` allows unauthenticated GET, HEAD, and LIST requests. Write operations (PUT, DELETE, multipart) always require authentication regardless of this setting. + +```toml +[[buckets]] +name = "public-data" +anonymous_access = true +``` diff --git a/docs/configuration/credentials.md b/docs/configuration/credentials.md new file mode 100644 index 0000000..f9cd425 --- /dev/null +++ b/docs/configuration/credentials.md @@ -0,0 +1,66 @@ +# Credentials + +Long-lived credentials are static access key pairs stored in the proxy configuration. They work like standard AWS IAM access keys — clients sign requests using SigV4 with the access key ID and secret access key. + +## Configuration + +```toml +[[credentials]] +access_key_id = "AKPROXY00000EXAMPLE" +secret_access_key = "proxy/secret/key/EXAMPLE000000000000" +principal_name = "internal-dashboard" +created_at = "2024-01-15T00:00:00Z" +enabled = true + +[[credentials.allowed_scopes]] +bucket = "public-data" +prefixes = [] +actions = ["get_object", "head_object", "list_bucket"] + +[[credentials.allowed_scopes]] +bucket = "ml-artifacts" +prefixes = ["models/production/"] +actions = ["get_object", "head_object"] +``` + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `access_key_id` | string | Yes | Access key identifier | +| `secret_access_key` | string | Yes | Secret key for SigV4 signing | +| `principal_name` | string | Yes | Human-readable name for the credential holder | +| `created_at` | datetime | Yes | When the credential was created (ISO 8601) | +| `expires_at` | datetime | No | When the credential expires (omit for no expiration) | +| `enabled` | bool | Yes | Whether the credential is active | +| `allowed_scopes` | AccessScope[] | Yes | Buckets, prefixes, and actions granted | + +## Access Scopes + +Scopes work identically to [role scopes](./roles#access-scopes) — each scope specifies a bucket, optional prefix restrictions, and allowed actions. + +## When to Use Long-Lived Credentials + +Long-lived credentials are appropriate for: + +- **Service accounts** that need persistent access without OIDC +- **Internal tools** where token exchange adds unnecessary complexity +- **Development and testing** environments +- **Environments without an OIDC provider** + +For CI/CD workflows and user-facing applications, prefer [OIDC/STS temporary credentials](/auth/proxy-auth#oidcsts-temporary-credentials) for better security (automatic expiration, no stored secrets). + +## Disabling Credentials + +Set `enabled = false` to immediately revoke access without removing the credential from config: + +```toml +[[credentials]] +access_key_id = "AKPROXY00000REVOKED" +secret_access_key = "..." +principal_name = "old-service" +created_at = "2023-01-01T00:00:00Z" +enabled = false +``` + +Disabled credentials return `AccessDenied` for any request. diff --git a/docs/configuration/index.md b/docs/configuration/index.md new file mode 100644 index 0000000..cd7afb0 --- /dev/null +++ b/docs/configuration/index.md @@ -0,0 +1,68 @@ +# Configuration + +The proxy configuration defines three things: + +1. **[Buckets](./buckets)** — Virtual buckets that map client-visible names to backend object stores +2. **[Roles](./roles)** — Trust policies for OIDC token exchange via `AssumeRoleWithWebIdentity` +3. **[Credentials](./credentials)** — Long-lived access keys for service accounts and internal tools + +```mermaid +flowchart TD + Config["Proxy Configuration"] + Config --> Buckets["Buckets\n(virtual names → backends)"] + Config --> Roles["Roles\n(OIDC trust policies)"] + Config --> Creds["Credentials\n(static access keys)"] + + Roles -- "allowed_scopes" --> Buckets + Creds -- "allowed_scopes" --> Buckets +``` + +## Config Format + +The server runtime uses TOML: + +```toml +[[buckets]] +name = "public-data" +backend_type = "s3" +anonymous_access = true + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-public-assets" +region = "us-east-1" +``` + +The CF Workers runtime uses JSON (as an environment variable or `wrangler.toml` object): + +```json +{ + "buckets": [{ + "name": "public-data", + "backend_type": "s3", + "anonymous_access": true, + "backend_options": { + "endpoint": "https://s3.us-east-1.amazonaws.com", + "bucket_name": "my-public-assets", + "region": "us-east-1" + } + }] +} +``` + +## Config Providers + +The proxy can load configuration from multiple backends. See [Config Providers](./providers/) for details. + +| Provider | Feature Flag | Use Case | +|----------|-------------|----------| +| [Static File](./providers/static-file) | (always available) | Simple deployments, baked-in config | +| [HTTP API](./providers/http) | `config-http` | Centralized config service | +| [DynamoDB](./providers/dynamodb) | `config-dynamodb` | AWS-native infrastructure | +| [PostgreSQL](./providers/postgres) | `config-postgres` | Database-backed config | + +All providers can be wrapped with a [cache](./providers/cached) for performance. + +## Full Example + +See the [annotated config example](/reference/config-example) for a complete configuration file with all options documented. diff --git a/docs/configuration/providers/cached.md b/docs/configuration/providers/cached.md new file mode 100644 index 0000000..a1597db --- /dev/null +++ b/docs/configuration/providers/cached.md @@ -0,0 +1,42 @@ +# Caching + +Wrap any config provider with `CachedProvider` to add in-memory TTL-based caching. This is recommended for all network-backed providers (HTTP, DynamoDB, PostgreSQL). + +## Usage + +```rust +use source_coop_core::config::cached::CachedProvider; +use std::time::Duration; + +let base = HttpProvider::new("https://config-api.internal".into(), None); +let provider = CachedProvider::new(base, Duration::from_secs(300)); +``` + +The first call hits the underlying provider; subsequent calls within the TTL return cached data. + +## Cache Behavior + +- **Thread-safe**: Uses `RwLock` internally, safe for concurrent access +- **Lazy eviction**: Expired entries are evicted on access, not proactively +- **Per-entity caching**: Each bucket, role, and credential is cached independently +- **Temporary credentials bypass**: Credential store/get operations for temporary credentials are not cached + +## Manual Invalidation + +```rust +// Invalidate everything +provider.invalidate_all(); + +// Invalidate a specific bucket +provider.invalidate_bucket("my-bucket"); +``` + +## Recommended TTLs + +| Provider | Suggested TTL | Rationale | +|----------|--------------|-----------| +| HTTP API | 60–300s | Balance between freshness and API load | +| DynamoDB | 60–300s | Reduce read capacity costs | +| PostgreSQL | 30–120s | Reduce query load | + +The server runtime's binary uses a 60-second TTL by default when wrapping the static file provider. diff --git a/docs/configuration/providers/dynamodb.md b/docs/configuration/providers/dynamodb.md new file mode 100644 index 0000000..59df1d0 --- /dev/null +++ b/docs/configuration/providers/dynamodb.md @@ -0,0 +1,33 @@ +# DynamoDB Provider + +The DynamoDB provider stores configuration in a single DynamoDB table using a PK/SK (partition key / sort key) design pattern. + +## Feature Flag + +```bash +cargo build -p source-coop-server --features source-coop-core/config-dynamodb +``` + +## Usage + +```rust +use source_coop_core::config::dynamodb::DynamoDbProvider; + +let aws_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; +let client = aws_sdk_dynamodb::Client::new(&aws_config); +let provider = DynamoDbProvider::new(client, "source-coop-proxy-config".to_string()); +``` + +## Table Design + +The provider uses a single-table design with partition key (`PK`) and sort key (`SK`) attributes. + +## When to Use + +- AWS-native infrastructure +- Serverless deployments where a database server isn't practical +- High-availability requirements (DynamoDB's built-in replication) + +::: tip +Wrap the DynamoDB provider with [CachedProvider](./cached) to reduce read costs and latency. +::: diff --git a/docs/configuration/providers/http.md b/docs/configuration/providers/http.md new file mode 100644 index 0000000..9451173 --- /dev/null +++ b/docs/configuration/providers/http.md @@ -0,0 +1,39 @@ +# HTTP API Provider + +The HTTP provider fetches configuration from a centralized REST API. Useful when you have a control plane service that manages proxy configuration. + +## Feature Flag + +```bash +cargo build -p source-coop-server --features source-coop-core/config-http +``` + +## Usage + +```rust +use source_coop_core::config::http::HttpProvider; + +let provider = HttpProvider::new( + "https://config-api.internal:8080".to_string(), + Some("Bearer my-api-token".to_string()), +); +``` + +## Expected API Endpoints + +The HTTP provider expects a REST API with these endpoints: + +| Endpoint | Method | Returns | +|----------|--------|---------| +| `/buckets` | GET | `Vec` | +| `/buckets/{name}` | GET | `Option` | +| `/roles/{id}` | GET | `Option` | +| `/credentials/{access_key_id}` | GET | `Option` | + +All responses should be JSON-encoded. Missing resources should return `null` or a 404 status. + +## When to Use + +- Centralized config management across multiple proxy instances +- Dynamic configuration that changes without proxy restarts (when combined with [caching](./cached)) +- Integration with a custom control plane or admin dashboard diff --git a/docs/configuration/providers/index.md b/docs/configuration/providers/index.md new file mode 100644 index 0000000..b40deef --- /dev/null +++ b/docs/configuration/providers/index.md @@ -0,0 +1,57 @@ +# Config Providers + +The proxy loads its configuration (buckets, roles, credentials) through the `ConfigProvider` trait. Multiple backends are available, selectable at build time via feature flags. + +## ConfigProvider Trait + +```rust +pub trait ConfigProvider: Clone + MaybeSend + MaybeSync + 'static { + async fn list_buckets(&self) -> Result, ProxyError>; + async fn get_bucket(&self, name: &str) -> Result, ProxyError>; + async fn get_role(&self, role_id: &str) -> Result, ProxyError>; + async fn get_credential(&self, access_key_id: &str) + -> Result, ProxyError>; +} +``` + +## Available Providers + +| Provider | Feature Flag | Best For | +|----------|-------------|----------| +| [Static File](./static-file) | (always available) | Simple deployments, single-file config | +| [HTTP API](./http) | `config-http` | Centralized config service, control planes | +| [DynamoDB](./dynamodb) | `config-dynamodb` | AWS-native infrastructure | +| [PostgreSQL](./postgres) | `config-postgres` | Database-backed config | + +All providers can be wrapped with [CachedProvider](./cached) for in-memory caching with TTL-based expiration. + +## Implementing a Custom Provider + +Implement the `ConfigProvider` trait and wrap it in `DefaultResolver` to get standard S3 proxy behavior: + +```rust +use source_coop_core::config::ConfigProvider; +use source_coop_core::error::ProxyError; +use source_coop_core::types::*; + +#[derive(Clone)] +struct MyProvider { /* ... */ } + +impl ConfigProvider for MyProvider { + async fn list_buckets(&self) -> Result, ProxyError> { + todo!() + } + async fn get_bucket(&self, name: &str) -> Result, ProxyError> { + todo!() + } + async fn get_role(&self, role_id: &str) -> Result, ProxyError> { + todo!() + } + async fn get_credential(&self, access_key_id: &str) + -> Result, ProxyError> { + todo!() + } +} +``` + +See [Custom Config Provider](/extending/custom-provider) for a full guide. diff --git a/docs/configuration/providers/postgres.md b/docs/configuration/providers/postgres.md new file mode 100644 index 0000000..f7516de --- /dev/null +++ b/docs/configuration/providers/postgres.md @@ -0,0 +1,28 @@ +# PostgreSQL Provider + +The PostgreSQL provider stores configuration in a PostgreSQL database using sqlx. + +## Feature Flag + +```bash +cargo build -p source-coop-server --features source-coop-core/config-postgres +``` + +## Usage + +```rust +use source_coop_core::config::postgres::PostgresProvider; + +let pool = sqlx::PgPool::connect("postgres://localhost/s3proxy").await?; +let provider = PostgresProvider::new(pool); +``` + +## When to Use + +- Existing PostgreSQL infrastructure +- Relational data management preferences +- Complex queries or joins with other application data + +::: tip +Wrap the PostgreSQL provider with [CachedProvider](./cached) to reduce query load and latency. +::: diff --git a/docs/configuration/providers/static-file.md b/docs/configuration/providers/static-file.md new file mode 100644 index 0000000..475be90 --- /dev/null +++ b/docs/configuration/providers/static-file.md @@ -0,0 +1,86 @@ +# Static File Provider + +The static file provider loads configuration from a TOML or JSON file at startup. No feature flags required — it's always available. + +## Usage + +```rust +use source_coop_core::config::static_file::StaticProvider; + +// From a TOML file +let provider = StaticProvider::from_file("config.toml")?; + +// From a TOML string +let provider = StaticProvider::from_toml(include_str!("../config.toml"))?; + +// From a JSON string (useful for CF Workers env vars) +let provider = StaticProvider::from_json(&json_string)?; +``` + +## Config Format + +### TOML + +```toml +[[buckets]] +name = "my-data" +backend_type = "s3" +anonymous_access = true + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-backend-bucket" +region = "us-east-1" + +[[roles]] +role_id = "my-role" +name = "My Role" +trusted_oidc_issuers = ["https://auth.example.com"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "my-data" +prefixes = [] +actions = ["get_object", "head_object"] + +[[credentials]] +access_key_id = "AKEXAMPLE" +secret_access_key = "secret" +principal_name = "service" +created_at = "2024-01-01T00:00:00Z" +enabled = true + +[[credentials.allowed_scopes]] +bucket = "my-data" +prefixes = [] +actions = ["get_object"] +``` + +### JSON + +```json +{ + "buckets": [{ + "name": "my-data", + "backend_type": "s3", + "anonymous_access": true, + "backend_options": { + "endpoint": "https://s3.us-east-1.amazonaws.com", + "bucket_name": "my-backend-bucket", + "region": "us-east-1" + } + }], + "roles": [], + "credentials": [] +} +``` + +## When to Use + +- Simple deployments with a single config file +- Baked-in configuration (e.g., compiled into the binary with `include_str!`) +- Cloudflare Workers (JSON via `PROXY_CONFIG` env var) +- Development and testing + +For dynamic configuration that changes without redeployment, consider [HTTP](./http), [DynamoDB](./dynamodb), or [PostgreSQL](./postgres) providers. diff --git a/docs/configuration/roles.md b/docs/configuration/roles.md new file mode 100644 index 0000000..4f5454d --- /dev/null +++ b/docs/configuration/roles.md @@ -0,0 +1,149 @@ +# Roles + +Roles define trust policies for OIDC token exchange via `AssumeRoleWithWebIdentity`. Each role specifies which identity providers to trust, what subject constraints to enforce, and what access scopes to grant. + +## Configuration + +```toml +[[roles]] +role_id = "github-actions-deployer" +name = "GitHub Actions Deploy Role" +trusted_oidc_issuers = ["https://token.actions.githubusercontent.com"] +required_audience = "sts.s3proxy.example.com" +subject_conditions = [ + "repo:myorg/myapp:ref:refs/heads/main", + "repo:myorg/infrastructure:*", +] +max_session_duration_secs = 3600 + +[[roles.allowed_scopes]] +bucket = "deploy-bundles" +prefixes = [] +actions = ["get_object", "head_object", "put_object"] + +[[roles.allowed_scopes]] +bucket = "ml-artifacts" +prefixes = ["models/", "datasets/"] +actions = ["get_object", "head_object"] +``` + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `role_id` | string | Yes | Identifier used as the `RoleArn` in STS requests | +| `name` | string | Yes | Human-readable display name | +| `trusted_oidc_issuers` | string[] | Yes | OIDC provider URLs whose tokens are accepted | +| `required_audience` | string | No | If set, the token's `aud` claim must match | +| `subject_conditions` | string[] | Yes | Glob patterns matched against the `sub` claim | +| `max_session_duration_secs` | integer | Yes | Maximum session lifetime (minimum 900s) | +| `allowed_scopes` | AccessScope[] | Yes | Buckets, prefixes, and actions granted | + +## Trust Policy Evaluation + +When a client calls `AssumeRoleWithWebIdentity`, the proxy evaluates the JWT against the role's trust policy in this order: + +1. **Issuer** — The JWT's `iss` claim must match one of `trusted_oidc_issuers` +2. **Algorithm** — Only RS256 is supported +3. **Signature** — Verified against the issuer's JWKS (fetched and cached) +4. **Audience** — If `required_audience` is set, the JWT's `aud` claim must match +5. **Subject** — The JWT's `sub` claim must match at least one `subject_conditions` pattern + +If any check fails, the STS request returns an error. + +## Subject Conditions + +Subject conditions use glob-style matching where `*` matches any sequence of characters: + +```toml +subject_conditions = [ + "repo:myorg/myapp:ref:refs/heads/main", # Exact match + "repo:myorg/myapp:ref:refs/heads/release/*", # Prefix match + "repo:myorg/*", # Any repo in the org + "*", # Any subject +] +``` + +The `sub` claim only needs to match one of the patterns. + +## Access Scopes + +Each scope grants access to a specific bucket with optional prefix and action restrictions: + +```toml +[[roles.allowed_scopes]] +bucket = "deploy-bundles" +prefixes = ["releases/", "staging/"] +actions = ["get_object", "head_object", "put_object"] +``` + +| Field | Type | Description | +|-------|------|-------------| +| `bucket` | string | Virtual bucket name (or template variable) | +| `prefixes` | string[] | Allowed key prefixes (empty = full bucket access) | +| `actions` | string[] | Allowed S3 operations | + +### Available Actions + +| Action | S3 Operation | +|--------|-------------| +| `get_object` | GET (download) | +| `head_object` | HEAD (metadata) | +| `put_object` | PUT (upload) | +| `delete_object` | DELETE | +| `list_bucket` | LIST (list objects) | +| `create_multipart_upload` | POST (initiate multipart) | +| `upload_part` | PUT with partNumber (upload part) | +| `complete_multipart_upload` | POST with uploadId (complete multipart) | +| `abort_multipart_upload` | DELETE with uploadId (abort multipart) | + +### Prefix Matching + +Prefix matching follows these rules: + +- If the prefix ends with `/` or is empty: the key must start with the prefix +- Otherwise: the key must equal the prefix exactly, or start with the prefix followed by `/` + +This prevents a prefix like `data` from accidentally matching `data-private/secret.txt`. The prefix `data/` would only match keys under the `data/` directory. + +## Template Variables + +Scope `bucket` and `prefixes` values support `{claim_name}` template variables that are resolved from the JWT claims at credential mint time: + +```toml +[[roles]] +role_id = "source-coop-user" +trusted_oidc_issuers = ["https://auth.source.coop"] +subject_conditions = ["*"] +max_session_duration_secs = 3600 + +# Each user gets access to a bucket matching their subject claim +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] +``` + +A user with `sub = "alice"` receives credentials scoped to `bucket = "alice"`. Any string claim from the JWT can be referenced — `{email}`, `{org}`, etc. + +Missing or non-string claims resolve to an empty string, which safely fails authorization. + +### Examples + +**Per-user bucket access:** +```toml +bucket = "{sub}" +``` + +**Organization-scoped prefix:** +```toml +bucket = "shared-data" +prefixes = ["{org}/"] +``` + +**Read-only access to all buckets:** +```toml +bucket = "*" +prefixes = [] +actions = ["get_object", "head_object", "list_bucket"] +``` diff --git a/docs/deployment/cloudflare-workers.md b/docs/deployment/cloudflare-workers.md new file mode 100644 index 0000000..8499468 --- /dev/null +++ b/docs/deployment/cloudflare-workers.md @@ -0,0 +1,95 @@ +# Cloudflare Workers + +The CF Workers runtime deploys the proxy to Cloudflare's edge network. It compiles to WASM and runs in the Workers V8 environment. + +## Limitations + +- **S3 backends only** — Azure and GCS are not supported on WASM +- **Static or API config only** — DynamoDB and Postgres providers require Tokio, which is unavailable +- **`SESSION_TOKEN_KEY` required** — Workers are stateless, so sealed tokens are the only way to persist temporary credentials + +## Configuration + +### `wrangler.toml` + +```toml +name = "source-coop-proxy" +main = "build/worker/shim.mjs" +compatibility_date = "2024-01-01" + +[build] +command = "cargo install worker-build && worker-build --release" + +[vars] +VIRTUAL_HOST_DOMAIN = "s3.example.com" + +[vars.PROXY_CONFIG] +buckets = [ + { name = "public-data", backend_type = "s3", anonymous_access = true, backend_options = { endpoint = "https://s3.us-east-1.amazonaws.com", bucket_name = "my-bucket", region = "us-east-1" } } +] +roles = [] +credentials = [] +``` + +`PROXY_CONFIG` can be either: +- A JSON string (via `wrangler secret put PROXY_CONFIG`) +- A JS object (via `[vars.PROXY_CONFIG]` table in `wrangler.toml`, as shown above) + +### Secrets + +Set sensitive values as secrets: + +```bash +wrangler secret put SESSION_TOKEN_KEY +wrangler secret put OIDC_PROVIDER_KEY +``` + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `PROXY_CONFIG` | Yes | JSON config (buckets, roles, credentials) | +| `VIRTUAL_HOST_DOMAIN` | No | Domain for virtual-hosted requests | +| `SESSION_TOKEN_KEY` | For STS | Base64-encoded 32-byte AES-256-GCM key | +| `OIDC_PROVIDER_KEY` | For OIDC backend auth | PEM-encoded RSA private key | +| `OIDC_PROVIDER_ISSUER` | For OIDC backend auth | Public URL for JWKS discovery | + +## Building + +```bash +# Check +cargo check -p source-coop-cf-workers --target wasm32-unknown-unknown + +# Build (via Wrangler) +cd crates/runtimes/cf-workers +npx wrangler build +``` + +::: warning +Always use `--target wasm32-unknown-unknown` when checking or building the CF Workers crate. It is excluded from the workspace `default-members` because WASM types won't compile on native targets. +::: + +## Development + +```bash +cd crates/runtimes/cf-workers +npx wrangler dev +``` + +This starts a local dev server on port `8787`. + +## Deploying + +```bash +cd crates/runtimes/cf-workers +npx wrangler deploy +``` + +## Source Cooperative Mode + +When `SOURCE_API_URL` is set, the Workers runtime uses `SourceCoopResolver` instead of `DefaultResolver`. This mode: +- Resolves backends dynamically from the Source Cooperative API +- Maps URLs as `/{account_id}/{repo_id}/{key}` instead of `/{bucket}/{key}` +- Handles authorization via the Source Cooperative API permissions endpoint + +This is specific to Source Cooperative deployments and is not needed for standalone proxy use. diff --git a/docs/deployment/index.md b/docs/deployment/index.md new file mode 100644 index 0000000..62e9b89 --- /dev/null +++ b/docs/deployment/index.md @@ -0,0 +1,13 @@ +# Deployment + +The proxy can be deployed in two ways: + +| | [Server Runtime](./server) | [Cloudflare Workers](./cloudflare-workers) | +|---|---|---| +| **Best for** | Container environments (ECS, K8s, Docker) | Edge deployments, low-latency global access | +| **Backends** | S3, Azure, GCS | S3 only | +| **Scaling** | Horizontal (multiple instances) | Automatic (Cloudflare edge) | +| **Config** | TOML file + env vars | Env vars (JSON) + Wrangler secrets | +| **Complexity** | Standard ops (containers, load balancers) | Managed (no infrastructure to operate) | + +Both runtimes use the same core logic and support the same authentication flows. Choose based on your infrastructure preferences and backend requirements. diff --git a/docs/deployment/server.md b/docs/deployment/server.md new file mode 100644 index 0000000..ee5df5f --- /dev/null +++ b/docs/deployment/server.md @@ -0,0 +1,81 @@ +# Server Runtime + +The server runtime uses Tokio and Hyper to run as a native HTTP server. It supports all backend providers (S3, Azure, GCS) and all config providers. + +## Building + +```bash +# Default build (S3 + Azure + GCS backends) +cargo build --release -p source-coop-server + +# With additional config providers +cargo build --release -p source-coop-server \ + --features source-coop-core/config-dynamodb \ + --features source-coop-core/config-postgres +``` + +The binary is located at `target/release/source-coop-proxy`. + +## Running + +```bash +./target/release/source-coop-proxy \ + --config config.toml \ + --listen 0.0.0.0:8080 +``` + +### CLI Arguments + +| Flag | Default | Description | +|------|---------|-------------| +| `--config` | (required) | Path to the TOML config file | +| `--listen` | `0.0.0.0:8080` | Address and port to listen on | +| `--domain` | (none) | Domain for virtual-hosted-style requests (e.g., `s3.example.com`) | +| `--sts-config` | (none) | Optional separate TOML file for STS roles/credentials | + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `SESSION_TOKEN_KEY` | For STS | Base64-encoded 32-byte AES-256-GCM key for sealed tokens | +| `OIDC_PROVIDER_KEY` | For OIDC backend auth | PEM-encoded RSA private key | +| `OIDC_PROVIDER_ISSUER` | For OIDC backend auth | Publicly reachable URL for JWKS discovery | +| `RUST_LOG` | No | Logging level (default: `source_coop=info`) | + +Generate a session token key: + +```bash +export SESSION_TOKEN_KEY=$(openssl rand -base64 32) +``` + +## Docker + +```bash +# Build +docker build -t source-coop-proxy . + +# Run +docker run \ + -v ./config.toml:/etc/source-coop-proxy/config.toml \ + -p 8080:8080 \ + -e SESSION_TOKEN_KEY="$SESSION_TOKEN_KEY" \ + source-coop-proxy +``` + +## Config Caching + +The server binary wraps the config provider with `CachedProvider` (60-second TTL). Config changes from network-backed providers (HTTP, DynamoDB, Postgres) are picked up within 60 seconds without restarting the proxy. + +For static file configs, changes require a restart. + +## Virtual-Hosted Style + +To support virtual-hosted-style requests (`bucket.s3.example.com/key`), use the `--domain` flag: + +```bash +./source-coop-proxy --config config.toml --domain s3.example.com +``` + +Configure DNS so that `*.s3.example.com` resolves to the proxy. The proxy extracts the bucket name from the `Host` header. + +Without `--domain`, only path-style requests are supported (`/bucket/key`). diff --git a/docs/extending/custom-backend.md b/docs/extending/custom-backend.md new file mode 100644 index 0000000..b38a606 --- /dev/null +++ b/docs/extending/custom-backend.md @@ -0,0 +1,120 @@ +# Custom Backend + +The `ProxyBackend` trait abstracts runtime-specific I/O. Implement it when deploying to a platform that's neither a standard server nor Cloudflare Workers. + +## The Trait + +```rust +use source_coop_core::backend::ProxyBackend; +use source_coop_core::types::BucketConfig; +use source_coop_core::error::ProxyError; +use object_store::{ObjectStore, signer::Signer}; +use std::sync::Arc; + +pub trait ProxyBackend: Clone + MaybeSend + MaybeSync + 'static { + /// Create an ObjectStore for LIST operations + fn create_store(&self, config: &BucketConfig) -> Result, ProxyError>; + + /// Create a Signer for presigned URL generation (GET/HEAD/PUT/DELETE) + fn create_signer(&self, config: &BucketConfig) -> Result, ProxyError>; + + /// Send a pre-signed HTTP request (multipart operations) + fn send_raw( + &self, + method: http::Method, + url: String, + headers: HeaderMap, + body: Bytes, + ) -> impl Future> + MaybeSend; +} +``` + +## Three Responsibilities + +### `create_store()` + +Returns an `Arc` used only for LIST operations. The runtime may need to inject a custom HTTP connector: + +```rust +fn create_store(&self, config: &BucketConfig) -> Result, ProxyError> { + // Use the shared helper, optionally injecting a custom connector + build_object_store(config, |builder| { + match builder { + StoreBuilder::S3(s) => StoreBuilder::S3(s.with_http_connector(MyConnector)), + other => other, + } + }) +} +``` + +### `create_signer()` + +Returns an `Arc` for generating presigned URLs. Signing is pure computation — no HTTP connector needed: + +```rust +fn create_signer(&self, config: &BucketConfig) -> Result, ProxyError> { + build_signer(config) +} +``` + +### `send_raw()` + +Executes a pre-signed HTTP request for multipart operations. Use your platform's HTTP client: + +```rust +async fn send_raw( + &self, + method: http::Method, + url: String, + headers: HeaderMap, + body: Bytes, +) -> Result { + let response = self.http_client + .request(method, &url) + .headers(headers) + .body(body) + .send() + .await + .map_err(|e| ProxyError::BackendError(e.to_string()))?; + + Ok(RawResponse { + status: response.status(), + headers: response.headers().clone(), + body: response.bytes().await + .map_err(|e| ProxyError::BackendError(e.to_string()))?, + }) +} +``` + +## Helper Functions + +The `backend` module provides shared helpers: + +- **`build_object_store(config, connector_fn)`** — Dispatches on `backend_type` ("s3", "az", "gcs"), iterates `backend_options` with `with_config()`, and applies the connector function +- **`build_signer(config)`** — Returns the appropriate signer: `object_store`'s built-in signer for authenticated backends, or `UnsignedUrlSigner` for anonymous backends + +These handle the multi-provider dispatch logic so your backend implementation only needs to provide the HTTP transport layer. + +## Wiring Into the Handler + +```rust +let backend = MyBackend::new(http_client); +let resolver = DefaultResolver::new(config_provider, token_key, domain); +let handler = ProxyHandler::new(backend, resolver); + +// In your request handler, handle all three action types: +match handler.resolve_request(method, path, query, &headers).await { + HandlerAction::Forward(fwd) => { + // Execute presigned URL with your HTTP client + // Stream request body (PUT) or response body (GET) + } + HandlerAction::Response(res) => { + // Return the complete response (LIST, errors) + } + HandlerAction::NeedsBody(pending) => { + // Collect request body, then: + let result = handler.handle_with_body(pending, body).await; + // Return the result + } +} +``` diff --git a/docs/extending/custom-provider.md b/docs/extending/custom-provider.md new file mode 100644 index 0000000..edaeaf2 --- /dev/null +++ b/docs/extending/custom-provider.md @@ -0,0 +1,104 @@ +# Custom Config Provider + +The `ConfigProvider` trait defines how the proxy loads buckets, roles, and credentials. Implement it to plug in your own configuration backend. + +## The Trait + +```rust +use source_coop_core::config::ConfigProvider; +use source_coop_core::error::ProxyError; +use source_coop_core::types::*; + +pub trait ConfigProvider: Clone + MaybeSend + MaybeSync + 'static { + async fn list_buckets(&self) -> Result, ProxyError>; + async fn get_bucket(&self, name: &str) -> Result, ProxyError>; + async fn get_role(&self, role_id: &str) -> Result, ProxyError>; + async fn get_credential(&self, access_key_id: &str) + -> Result, ProxyError>; +} +``` + +## Example: Redis Provider + +```rust +use source_coop_core::config::ConfigProvider; +use source_coop_core::error::ProxyError; +use source_coop_core::types::*; + +#[derive(Clone)] +struct RedisProvider { + client: redis::Client, +} + +impl ConfigProvider for RedisProvider { + async fn list_buckets(&self) -> Result, ProxyError> { + let mut conn = self.client.get_async_connection().await + .map_err(|e| ProxyError::Internal(e.to_string()))?; + + let keys: Vec = redis::cmd("KEYS") + .arg("bucket:*") + .query_async(&mut conn) + .await + .map_err(|e| ProxyError::Internal(e.to_string()))?; + + let mut buckets = Vec::new(); + for key in keys { + let json: String = redis::cmd("GET") + .arg(&key) + .query_async(&mut conn) + .await + .map_err(|e| ProxyError::Internal(e.to_string()))?; + let bucket: BucketConfig = serde_json::from_str(&json) + .map_err(|e| ProxyError::ConfigError(e.to_string()))?; + buckets.push(bucket); + } + Ok(buckets) + } + + async fn get_bucket(&self, name: &str) -> Result, ProxyError> { + // Similar Redis GET with key "bucket:{name}" + todo!() + } + + async fn get_role(&self, role_id: &str) -> Result, ProxyError> { + todo!() + } + + async fn get_credential(&self, access_key_id: &str) + -> Result, ProxyError> { + todo!() + } +} +``` + +## Using with DefaultResolver + +Wrap your provider in `DefaultResolver` to get standard S3 proxy behavior (path/virtual-host parsing, SigV4 auth, scope-based authorization): + +```rust +use source_coop_core::resolver::DefaultResolver; +use source_coop_core::config::cached::CachedProvider; +use std::time::Duration; + +// Optional: wrap with caching +let cached = CachedProvider::new(redis_provider, Duration::from_secs(60)); + +// Create resolver with optional token key and domain +let resolver = DefaultResolver::new(cached, token_key, virtual_host_domain); + +// Wire into the proxy handler +let handler = ProxyHandler::new(backend, resolver); +``` + +## Using with CachedProvider + +For network-backed providers, wrap with `CachedProvider` to reduce latency: + +```rust +use source_coop_core::config::cached::CachedProvider; +use std::time::Duration; + +let provider = CachedProvider::new(redis_provider, Duration::from_secs(120)); +``` + +See [Caching](/configuration/providers/cached) for cache behavior details. diff --git a/docs/extending/custom-resolver.md b/docs/extending/custom-resolver.md new file mode 100644 index 0000000..ec78735 --- /dev/null +++ b/docs/extending/custom-resolver.md @@ -0,0 +1,128 @@ +# Custom Request Resolver + +The `RequestResolver` trait controls how incoming requests are parsed, authenticated, and authorized. Implement it for full control over the request handling pipeline. + +## The Trait + +```rust +use source_coop_core::resolver::{RequestResolver, ResolvedAction}; +use source_coop_core::error::ProxyError; +use http::{Method, HeaderMap}; + +pub trait RequestResolver: Clone + MaybeSend + MaybeSync + 'static { + fn resolve( + &self, + method: &Method, + path: &str, + query: Option<&str>, + headers: &HeaderMap, + ) -> impl Future> + MaybeSend; +} +``` + +## ResolvedAction + +The resolver returns one of two actions: + +```rust +pub enum ResolvedAction { + /// Forward to a backend (standard proxy behavior) + Proxy { + operation: S3Operation, + bucket_config: BucketConfig, + list_rewrite: Option, + }, + /// Return a synthetic response (e.g., virtual listing, redirect) + Response { + status: StatusCode, + headers: HeaderMap, + body: ProxyResponseBody, + }, +} +``` + +## Example: Custom Namespace + +A resolver that maps `/{account}/{repo}/{key}` to backend buckets: + +```rust +use source_coop_core::resolver::{RequestResolver, ResolvedAction}; +use source_coop_core::s3::request::build_s3_operation; +use source_coop_core::error::ProxyError; + +#[derive(Clone)] +struct MyResolver { + api_client: ApiClient, +} + +impl RequestResolver for MyResolver { + async fn resolve( + &self, + method: &Method, + path: &str, + query: Option<&str>, + headers: &HeaderMap, + ) -> Result { + // Parse custom URL structure + let parts: Vec<&str> = path.trim_start_matches('/').splitn(3, '/').collect(); + let (account, repo, key) = match parts.as_slice() { + [a, r, k] => (*a, *r, *k), + [a, r] => (*a, *r, ""), + _ => return Err(ProxyError::BucketNotFound), + }; + + // Look up the backend config from an external API + let bucket_config = self.api_client + .get_backend(account, repo) + .await + .map_err(|_| ProxyError::BucketNotFound)?; + + // Authenticate via external service + self.api_client + .check_permissions(account, repo, headers) + .await + .map_err(|_| ProxyError::AccessDenied)?; + + // Build the S3 operation from method + key + let operation = build_s3_operation(method, &bucket_config.name, key, query)?; + + Ok(ResolvedAction::Proxy { + operation, + bucket_config, + list_rewrite: None, + }) + } +} +``` + +## Wiring Into the Handler + +```rust +let resolver = MyResolver::new(api_client); +let handler = ProxyHandler::new(backend, resolver); + +// In your request handler: +let action = handler.resolve_request(method, path, query, &headers).await; +match action { + HandlerAction::Forward(fwd) => { /* execute presigned URL */ } + HandlerAction::Response(res) => { /* return response */ } + HandlerAction::NeedsBody(pending) => { /* collect body, call handle_with_body */ } +} +``` + +## ListRewrite + +The `ListRewrite` option in `ResolvedAction::Proxy` allows you to transform `` and `` values in LIST response XML: + +```rust +ResolvedAction::Proxy { + operation, + bucket_config, + list_rewrite: Some(ListRewrite { + strip_prefix: "internal/mirror/".to_string(), + add_prefix: "public/".to_string(), + }), +} +``` + +This is useful when the backend key structure differs from what clients expect. diff --git a/docs/extending/index.md b/docs/extending/index.md new file mode 100644 index 0000000..2c64b09 --- /dev/null +++ b/docs/extending/index.md @@ -0,0 +1,17 @@ +# Extending the Proxy + +The proxy is designed for customization through three trait boundaries. Each controls a different aspect of the proxy's behavior. + +| Trait | Controls | Default Implementation | +|-------|----------|----------------------| +| [RequestResolver](./custom-resolver) | How requests are parsed, authenticated, and authorized | `DefaultResolver` (standard S3 proxy behavior) | +| [ConfigProvider](./custom-provider) | Where configuration comes from | Static file, HTTP, DynamoDB, Postgres | +| [ProxyBackend](./custom-backend) | How the runtime interacts with backends | `ServerBackend`, `WorkerBackend` | + +## When to Customize What + +**Custom Resolver** — Your URL namespace doesn't map to `/{bucket}/{key}`, or you need external authorization (e.g., an API call), or you want different authentication logic. + +**Custom Config Provider** — You want to store config in a backend not already supported (e.g., etcd, Redis, Consul), or you need to derive config from another source. + +**Custom Backend** — You're deploying to a runtime that's neither a standard server nor Cloudflare Workers (e.g., AWS Lambda, Deno Deploy), or you need a different HTTP client. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..0d5ea6a --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,63 @@ +# Quick Start + +This guide is for administrators setting up and running the Source Data Proxy. If you're a user looking to access data through an existing proxy, see the [User Guide](/guide/). + +The Source Data Proxy is a multi-runtime S3 gateway that proxies requests to backend object stores. This guide gets you running locally in minutes. + +## Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (latest stable) +- [Docker](https://docs.docker.com/get-docker/) (for local development with MinIO) + +## Start the Backend + +Use Docker Compose to start MinIO as a local object store: + +```bash +docker compose up +``` + +This starts: +- MinIO API on port `9000` +- MinIO Console on port `9001` (user: `minioadmin`, password: `minioadmin`) +- A seed job that creates example buckets with test data + +## Run the Proxy + +Choose either the native server runtime or Cloudflare Workers: + +::: code-group + +```bash [Server Runtime] +cargo run -p source-coop-server -- \ + --config config.local.toml \ + --listen 0.0.0.0:8080 +``` + +```bash [Cloudflare Workers] +cd crates/runtimes/cf-workers && npx wrangler dev +``` + +::: + +The server runtime listens on port `8080`. The Workers runtime listens on port `8787`. + +## Make Your First Request + +```bash +# Anonymous read from a public bucket +curl http://localhost:8080/public-data/hello.txt + +# Signed upload with the local dev credential +AWS_ACCESS_KEY_ID=AKLOCAL0000000000001 \ +AWS_SECRET_ACCESS_KEY="localdev/secret/key/00000000000000000000" \ +aws s3 cp ./myfile.txt s3://private-uploads/myfile.txt \ + --endpoint-url http://localhost:8080 +``` + +## Next Steps + +- [Local Development](./local-development) — Detailed dev environment setup +- [Configuration](/configuration/) — Configuring buckets, roles, and credentials +- [Authentication](/auth/) — Setting up auth flows +- [Deployment](/deployment/) — Deploying to production diff --git a/docs/getting-started/local-development.md b/docs/getting-started/local-development.md new file mode 100644 index 0000000..22e242b --- /dev/null +++ b/docs/getting-started/local-development.md @@ -0,0 +1,105 @@ +# Local Development + +This guide walks through setting up a full local development environment with MinIO as the backing object store. + +## Docker Compose + +The project includes a `docker-compose.yml` that starts MinIO and seeds it with example data: + +```bash +docker compose up +``` + +This starts: +- **MinIO API** at `http://localhost:9000` +- **MinIO Console** at `http://localhost:9001` (credentials: `minioadmin` / `minioadmin`) +- A seed job that creates `public-data` and `private-uploads` buckets with sample files + +## Configuration Files + +The two runtimes use different config formats: + +### Server Runtime — `config.local.toml` + +The server runtime reads a TOML config file. The local development config points buckets at `http://localhost:9000` (MinIO): + +```bash +cargo run -p source-coop-server -- \ + --config config.local.toml \ + --listen 0.0.0.0:8080 +``` + +### Workers Runtime — `wrangler.toml` + +The CF Workers runtime reads `PROXY_CONFIG` from the Wrangler configuration. It can be a JSON string or a JS object: + +```bash +cd crates/runtimes/cf-workers && npx wrangler dev +``` + +The Workers dev server runs on port `8787` by default. + +## Building + +```bash +# Check/build default workspace members (excludes cf-workers) +cargo check +cargo build + +# CF Workers must target wasm32 +cargo check -p source-coop-cf-workers --target wasm32-unknown-unknown + +# Run tests +cargo test +``` + +## Makefile + +The project includes a Makefile with common tasks: + +```bash +make check # cargo check +make check-wasm # cargo check for CF Workers (wasm32 target) +make test # cargo test +make fmt # check formatting +make clippy # run linter +make run-server # run the server runtime +make run-workers # run the workers runtime (wrangler dev) +make ci-fast # fmt + clippy + check-wasm +make ci # ci-fast + test +``` + +## Environment Variables + +For local development, these are optional but useful: + +| Variable | Purpose | Example | +|----------|---------|---------| +| `SESSION_TOKEN_KEY` | AES-256-GCM key for sealed tokens | `openssl rand -base64 32` | +| `OIDC_PROVIDER_KEY` | RSA private key for OIDC backend auth | PEM file contents | +| `OIDC_PROVIDER_ISSUER` | Public URL for OIDC discovery | `http://localhost:8080` | +| `RUST_LOG` | Logging level | `source_coop=debug` | + +## Verifying the Setup + +Once the proxy is running, test both anonymous and authenticated access: + +```bash +# Anonymous read (should return file contents) +curl http://localhost:8080/public-data/hello.txt + +# Authenticated upload +AWS_ACCESS_KEY_ID=AKLOCAL0000000000001 \ +AWS_SECRET_ACCESS_KEY="localdev/secret/key/00000000000000000000" \ +aws s3 cp ./test.txt s3://private-uploads/test.txt \ + --endpoint-url http://localhost:8080 + +# List bucket contents +AWS_ACCESS_KEY_ID=AKLOCAL0000000000001 \ +AWS_SECRET_ACCESS_KEY="localdev/secret/key/00000000000000000000" \ +aws s3 ls s3://private-uploads/ \ + --endpoint-url http://localhost:8080 + +# Browse MinIO directly +open http://localhost:9001 +``` diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md new file mode 100644 index 0000000..82ab47c --- /dev/null +++ b/docs/guide/authentication.md @@ -0,0 +1,135 @@ +# Authentication + +The proxy supports three ways to authenticate, depending on your use case. + +## Anonymous Access + +Public buckets serve read requests without credentials: + +```bash +curl https://data.source.coop/public-data/hello.txt +``` + +Anonymous access only allows `GetObject`, `HeadObject`, and `ListBucket` operations. Write operations always require authentication. + +## Long-Lived Access Keys + +If your administrator has issued you a static access key pair, use them like standard AWS credentials: + +```bash +AWS_ACCESS_KEY_ID=AKPROXY00000EXAMPLE \ +AWS_SECRET_ACCESS_KEY="proxy/secret/key/EXAMPLE000000000000" \ +aws s3 cp s3://my-bucket/path/to/file.txt ./file.txt \ + --endpoint-url https://data.source.coop +``` + +These work with any S3-compatible client. The proxy verifies requests using standard AWS SigV4 signing, so no special client configuration is needed beyond setting the endpoint URL. + +## OIDC / STS Temporary Credentials + +This is the recommended authentication method. You exchange a JWT from your organization's identity provider for scoped, time-limited credentials — the same flow as AWS `AssumeRoleWithWebIdentity`. + +There are two ways to do this: the CLI (for interactive use) and direct STS calls (for CI/CD and scripts). + +### CLI Authentication + +The `source-coop` CLI handles the OIDC flow for you. It opens your browser, authenticates with your identity provider, and obtains temporary credentials. + +**Install the CLI:** + +```bash +cargo install --path crates/cli +``` + +**Log in:** + +```bash +source-coop login +``` + +This opens your browser to authenticate. Once complete, credentials are cached locally. + +### AWS Profile Integration + +Set up an AWS profile to use the proxy seamlessly with standard AWS tools: + +```ini +[profile source-coop] +credential_process = source-coop credential-process +endpoint_url = https://data.source.coop +``` + +For local development, override the proxy URL: + +```ini +[profile sc-local] +credential_process = source-coop credential-process --proxy-url http://localhost:8787 +endpoint_url = http://localhost:8787 +``` + +Then use AWS tools normally — credentials are obtained and refreshed automatically: + +```bash +aws s3 ls s3://my-bucket/ --profile source-coop +aws s3 cp ./data.csv s3://my-bucket/uploads/data.csv --profile source-coop +``` + +### Multiple Roles + +If your administrator has set up multiple roles with different access scopes, you can create a profile for each: + +```bash +source-coop login --role-arn reader-role +source-coop login --role-arn admin-role +``` + +```ini +[profile sc-reader] +credential_process = source-coop credential-process --role-arn reader-role +endpoint_url = https://data.source.coop + +[profile sc-admin] +credential_process = source-coop credential-process --role-arn admin-role +endpoint_url = https://data.source.coop +``` + +### CLI Options + +| Flag | Env Var | Default | Description | +|------|---------|---------|-------------| +| `--issuer` | `SOURCE_OIDC_ISSUER` | `https://auth.source.coop` | OIDC issuer URL | +| `--client-id` | `SOURCE_OIDC_CLIENT_ID` | (built-in) | OAuth2 client ID | +| `--proxy-url` | `SOURCE_PROXY_URL` | `https://data.source.coop` | Proxy URL for STS | +| `--role-arn` | `SOURCE_ROLE_ARN` | `source-coop-user` | Role ARN to assume | +| `--format` | | `credential-process` | Output: `credential-process` or `env` | +| `--duration` | | (role default) | Session duration in seconds | +| `--scope` | | `openid` | OAuth2 scopes | +| `--port` | | `0` (random) | Local callback server port | + +### Direct STS Exchange + +For CI/CD pipelines and scripts, the CLI can output credentials as environment variables using `--format env`: + +```bash +eval $(source-coop login --format env) + +# Credentials are now exported — use any S3 client +aws s3 cp ./data.csv s3://deploy-bundles/data.csv \ + --endpoint-url https://data.source.coop +``` + +You can also call the STS endpoint directly with a JWT: + +```bash +CREDS=$(aws sts assume-role-with-web-identity \ + --role-arn github-actions-deployer \ + --web-identity-token "$JWT_TOKEN" \ + --endpoint-url https://data.source.coop \ + --output json) + +export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId') +export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey') +export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken') +``` + +The STS endpoint accepts a JWT from any OIDC provider that your administrator has configured as trusted. See the [Administration guide](/auth/proxy-auth) for details on setting up identity providers and trust policies. diff --git a/docs/guide/client-usage.md b/docs/guide/client-usage.md new file mode 100644 index 0000000..638d834 --- /dev/null +++ b/docs/guide/client-usage.md @@ -0,0 +1,109 @@ +# Client Usage + +The proxy exposes a standard S3-compatible API. Any S3 client works — just set the endpoint URL to point at the proxy. + +## aws-cli + +```bash +# Download a file +aws s3 cp s3://my-bucket/path/to/file.txt ./file.txt \ + --endpoint-url https://data.source.coop + +# Upload a file +aws s3 cp ./local-file.txt s3://my-bucket/uploads/file.txt \ + --endpoint-url https://data.source.coop + +# List bucket contents +aws s3 ls s3://my-bucket/prefix/ \ + --endpoint-url https://data.source.coop +``` + +### Using AWS Profiles + +Add a profile to `~/.aws/config` to avoid specifying the endpoint every time: + +```ini +[profile source-coop] +credential_process = source-coop credential-process +endpoint_url = https://data.source.coop +``` + +Then use it: + +```bash +aws s3 ls s3://my-bucket/ --profile source-coop +aws s3 cp s3://my-bucket/data.csv ./data.csv --profile source-coop +``` + +See [Authentication](./authentication) for setting up credentials and profiles. + +## boto3 (Python) + +```python +import boto3 + +s3 = boto3.client( + "s3", + endpoint_url="https://data.source.coop", + aws_access_key_id="AKPROXY00000EXAMPLE", + aws_secret_access_key="proxy/secret/key/EXAMPLE", +) + +# Download +s3.download_file("my-bucket", "path/to/file.txt", "./file.txt") + +# Upload +s3.upload_file("./local-file.txt", "my-bucket", "uploads/file.txt") + +# List +response = s3.list_objects_v2(Bucket="my-bucket", Prefix="prefix/") +for obj in response.get("Contents", []): + print(obj["Key"]) +``` + +### Using a Profile with boto3 + +If you have an AWS profile configured with `credential_process`: + +```python +import boto3 + +session = boto3.Session(profile_name="source-coop") +s3 = session.client("s3") + +response = s3.list_objects_v2(Bucket="my-bucket") +``` + +## curl + +For anonymous buckets, you can use curl directly: + +```bash +# Download +curl https://data.source.coop/public-data/hello.txt + +# HEAD request (metadata only) +curl -I https://data.source.coop/public-data/hello.txt +``` + +For authenticated requests, use aws-cli or an SDK that handles SigV4 signing. + +## Request Styles + +The proxy supports two S3 URL styles: + +### Path Style (default) + +``` +https://data.source.coop/bucket-name/key/path +``` + +This is the default and works without additional configuration. + +### Virtual-Hosted Style + +``` +https://bucket-name.s3.example.com/key/path +``` + +Virtual-hosted style requires that the proxy administrator has configured the `--domain` flag. The proxy extracts the bucket name from the `Host` header. diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..6d390fa --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,23 @@ +# User Guide + +The Source Data Proxy provides S3-compatible access to data stored across multiple cloud backends. You interact with it using standard S3 tools — `aws-cli`, `boto3`, or any S3-compatible SDK — just point the endpoint URL at the proxy. + +## Getting Started + +1. **[Authentication](./authentication)** — How to authenticate and obtain credentials +2. **[Client Usage](./client-usage)** — Using aws-cli, boto3, curl, and other S3 clients + +## Quick Example + +```bash +# Anonymous access to a public bucket +curl https://data.source.coop/public-data/hello.txt + +# Authenticated access with the CLI +source-coop login +aws s3 ls s3://my-bucket/ --profile source-coop +``` + +## How It Works + +The proxy sits between your S3 client and the backend object stores. You send standard S3 requests to the proxy, and it handles authentication, authorization, and forwarding to the correct backend. From your perspective, it behaves like any other S3-compatible service. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7b30baf --- /dev/null +++ b/docs/index.md @@ -0,0 +1,52 @@ +--- +layout: home + +hero: + name: Source Data Proxy + text: Multi-runtime S3 gateway proxy + tagline: A Radiant Earth project. Stream S3-compatible requests to backend object stores with authentication, authorization, and zero-copy passthrough. + actions: + - theme: brand + text: User Guide + link: /guide/ + - theme: alt + text: Administration + link: /getting-started/ + - theme: alt + text: View on GitHub + link: https://github.com/source-cooperative/data.source.coop + +features: + - title: Multi-Runtime + details: Deploy as a native Tokio/Hyper server in containers, or as a Cloudflare Worker at the edge. Same core logic, different runtimes. + - title: Multi-Provider + details: Proxy to AWS S3, MinIO, Cloudflare R2, Azure Blob Storage, or Google Cloud Storage through a unified S3-compatible API. + - title: OIDC/STS Authentication + details: Exchange OIDC tokens from any identity provider (GitHub Actions, Auth0, Keycloak, Cognito, Ory) for scoped temporary credentials via AssumeRoleWithWebIdentity. + - title: Zero-Copy Streaming + details: Presigned URLs enable direct streaming between clients and backends. No buffering, no double-handling of request or response bodies. + - title: Modular Architecture + details: Compose your own proxy with pluggable traits for backends, config providers, and request resolvers. Use the defaults or bring your own. + - title: OIDC Backend Auth + details: The proxy acts as its own OIDC identity provider to authenticate with cloud backends — no long-lived credentials needed. +--- + +## How It Works + +```mermaid +flowchart LR + Clients["S3 Clients\n(aws-cli, boto3, SDKs)"] + + subgraph Proxy["source-coop-proxy"] + Auth["Auth\n(STS, OIDC, SigV4)"] + Core["Core\n(Proxy Handler)"] + Config["Config\n(Static, HTTP, DynamoDB, Postgres)"] + end + + Backend["Backend Stores\n(AWS S3, MinIO, R2, Azure, GCS)"] + + Clients <--> Proxy + Proxy <--> Backend +``` + +The proxy sits between S3-compatible clients and backend object stores. It authenticates incoming requests, authorizes them against configured scopes, and forwards them to the appropriate backend using presigned URLs for zero-copy streaming. diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..7f2004e --- /dev/null +++ b/docs/package.json @@ -0,0 +1,20 @@ +{ + "name": "source-data-proxy-docs", + "private": true, + "type": "module", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "@braintree/sanitize-url": "^7.1.2", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "mermaid": "^11.4.1", + "vitepress": "^1.6.3", + "vitepress-plugin-mermaid": "^2.0.17" + } +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 0000000..5376ad6 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,2620 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@braintree/sanitize-url': + specifier: ^7.1.2 + version: 7.1.2 + cytoscape: + specifier: ^3.33.1 + version: 3.33.1 + cytoscape-cose-bilkent: + specifier: ^4.1.0 + version: 4.1.0(cytoscape@3.33.1) + dayjs: + specifier: ^1.11.19 + version: 1.11.19 + debug: + specifier: ^4.4.3 + version: 4.4.3 + mermaid: + specifier: ^11.4.1 + version: 11.12.3 + vitepress: + specifier: ^1.6.3 + version: 1.6.4(@algolia/client-search@5.49.1)(postcss@8.5.6)(search-insights@2.17.3) + vitepress-plugin-mermaid: + specifier: ^2.0.17 + version: 2.0.17(mermaid@11.12.3)(vitepress@1.6.4(@algolia/client-search@5.49.1)(postcss@8.5.6)(search-insights@2.17.3)) + +packages: + + '@algolia/abtesting@1.15.1': + resolution: {integrity: sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.49.1': + resolution: {integrity: sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.49.1': + resolution: {integrity: sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.49.1': + resolution: {integrity: sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.49.1': + resolution: {integrity: sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.49.1': + resolution: {integrity: sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.49.1': + resolution: {integrity: sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.49.1': + resolution: {integrity: sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.49.1': + resolution: {integrity: sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.49.1': + resolution: {integrity: sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.49.1': + resolution: {integrity: sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.49.1': + resolution: {integrity: sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.49.1': + resolution: {integrity: sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.49.1': + resolution: {integrity: sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA==} + engines: {node: '>= 14.0.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/cst-dts-gen@11.1.2': + resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} + + '@chevrotain/gast@11.1.2': + resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} + + '@chevrotain/regexp-to-ast@11.1.2': + resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@chevrotain/utils@11.1.2': + resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.71': + resolution: {integrity: sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@mermaid-js/mermaid-mindmap@9.3.0': + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + + '@mermaid-js/parser@1.0.0': + resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/reactivity@3.5.29': + resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + + '@vue/runtime-core@3.5.29': + resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + + '@vue/runtime-dom@3.5.29': + resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + + '@vue/server-renderer@3.5.29': + resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + peerDependencies: + vue: 3.5.29 + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + algoliasearch@5.49.1: + resolution: {integrity: sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==} + engines: {node: '>= 14.0.0'} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.1.2: + resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + katex@0.16.33: + resolution: {integrity: sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + langium@4.2.1: + resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mermaid@11.12.3: + resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.28.4: + resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress-plugin-mermaid@2.0.17: + resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue@3.5.29: + resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/abtesting@1.15.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1) + '@algolia/client-search': 5.49.1 + algoliasearch: 5.49.1 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)': + dependencies: + '@algolia/client-search': 5.49.1 + algoliasearch: 5.49.1 + + '@algolia/client-abtesting@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-analytics@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-common@5.49.1': {} + + '@algolia/client-insights@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-personalization@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-query-suggestions@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/client-search@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/ingestion@1.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/monitoring@1.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/recommend@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + '@algolia/requester-browser-xhr@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + + '@algolia/requester-fetch@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + + '@algolia/requester-node-http@5.49.1': + dependencies: + '@algolia/client-common': 5.49.1 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@braintree/sanitize-url@6.0.4': + optional: true + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/cst-dts-gen@11.1.2': + dependencies: + '@chevrotain/gast': 11.1.2 + '@chevrotain/types': 11.1.2 + lodash-es: 4.17.23 + + '@chevrotain/gast@11.1.2': + dependencies: + '@chevrotain/types': 11.1.2 + lodash-es: 4.17.23 + + '@chevrotain/regexp-to-ast@11.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@chevrotain/utils@11.1.2': {} + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.49.1)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.49.1)(search-insights@2.17.3) + preact: 10.28.4 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.49.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.49.1)(algoliasearch@5.49.1) + '@docsearch/css': 3.8.2 + algoliasearch: 5.49.1 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.71': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@mermaid-js/mermaid-mindmap@9.3.0': + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + + '@mermaid-js/parser@1.0.0': + dependencies: + langium: 4.2.1 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.29)': + dependencies: + vite: 5.4.21 + vue: 3.5.29 + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.29': + dependencies: + '@vue/shared': 3.5.29 + + '@vue/runtime-core@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/runtime-dom@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/runtime-core': 3.5.29 + '@vue/shared': 3.5.29 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.29(vue@3.5.29)': + dependencies: + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + vue: 3.5.29 + + '@vue/shared@3.5.29': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.29 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.8.0)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.29 + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.29 + transitivePeerDependencies: + - typescript + + acorn@8.16.0: {} + + algoliasearch@5.49.1: + dependencies: + '@algolia/abtesting': 1.15.1 + '@algolia/client-abtesting': 5.49.1 + '@algolia/client-analytics': 5.49.1 + '@algolia/client-common': 5.49.1 + '@algolia/client-insights': 5.49.1 + '@algolia/client-personalization': 5.49.1 + '@algolia/client-query-suggestions': 5.49.1 + '@algolia/client-search': 5.49.1 + '@algolia/ingestion': 1.49.1 + '@algolia/monitoring': 1.49.1 + '@algolia/recommend': 5.49.1 + '@algolia/requester-browser-xhr': 5.49.1 + '@algolia/requester-fetch': 5.49.1 + '@algolia/requester-node-http': 5.49.1 + + birpc@2.9.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + chevrotain-allstar@0.3.1(chevrotain@11.1.2): + dependencies: + chevrotain: 11.1.2 + lodash-es: 4.17.23 + + chevrotain@11.1.2: + dependencies: + '@chevrotain/cst-dts-gen': 11.1.2 + '@chevrotain/gast': 11.1.2 + '@chevrotain/regexp-to-ast': 11.1.2 + '@chevrotain/types': 11.1.2 + '@chevrotain/utils': 11.1.2 + lodash-es: 4.17.23 + + comma-separated-tokens@2.0.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + confbox@0.1.8: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + + dayjs@1.11.19: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-regex-xs@1.0.0: {} + + entities@7.0.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + + fsevents@2.3.3: + optional: true + + hachure-fill@0.5.2: {} + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-what@5.5.0: {} + + katex@0.16.33: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + langium@4.2.1: + dependencies: + chevrotain: 11.1.2 + chevrotain-allstar: 0.3.1(chevrotain@11.1.2) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lodash-es@4.17.23: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + marked@16.4.2: {} + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mermaid@11.12.3: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 1.0.0 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.33 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + non-layered-tidy-tree-layout@2.0.2: + optional: true + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + + package-manager-detector@1.6.0: {} + + path-data-parser@0.1.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.28.4: {} + + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + robust-predicates@3.0.2: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + search-insights@2.17.3: {} + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stylis@4.3.6: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tabbable@6.4.0: {} + + tinyexec@1.0.2: {} + + trim-lines@3.0.1: {} + + ts-dedent@2.2.0: {} + + ufo@1.6.3: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uuid@11.1.0: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.59.0 + optionalDependencies: + fsevents: 2.3.3 + + vitepress-plugin-mermaid@2.0.17(mermaid@11.12.3)(vitepress@1.6.4(@algolia/client-search@5.49.1)(postcss@8.5.6)(search-insights@2.17.3)): + dependencies: + mermaid: 11.12.3 + vitepress: 1.6.4(@algolia/client-search@5.49.1)(postcss@8.5.6)(search-insights@2.17.3) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + + vitepress@1.6.4(@algolia/client-search@5.49.1)(postcss@8.5.6)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.49.1)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.71 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21)(vue@3.5.29) + '@vue/devtools-api': 7.7.9 + '@vue/shared': 3.5.29 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.8.0) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21 + vue: 3.5.29 + optionalDependencies: + postcss: 8.5.6 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + + vue@3.5.29: + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-sfc': 3.5.29 + '@vue/runtime-dom': 3.5.29 + '@vue/server-renderer': 3.5.29(vue@3.5.29) + '@vue/shared': 3.5.29 + + zwitch@2.0.4: {} diff --git a/docs/reference/config-example.md b/docs/reference/config-example.md new file mode 100644 index 0000000..b97b264 --- /dev/null +++ b/docs/reference/config-example.md @@ -0,0 +1,164 @@ +# Configuration Example + +A complete, annotated configuration file showing all available options. + +```toml +# ============================================================================= +# Virtual Buckets +# ============================================================================= + +# A publicly accessible S3 bucket (anonymous reads allowed) +[[buckets]] +name = "public-data" # Client-visible bucket name +backend_type = "s3" # Backend provider: "s3", "az", or "gcs" +anonymous_access = true # Allow GET/HEAD/LIST without auth +allowed_roles = [] # No STS roles (anonymous only) + +[buckets.backend_options] +endpoint = "https://s3.us-east-1.amazonaws.com" +bucket_name = "my-company-public-assets" # Actual backend bucket name +region = "us-east-1" +access_key_id = "AKIAIOSFODNN7EXAMPLE" +secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + +# A private S3 bucket backed by MinIO with a backend prefix +[[buckets]] +name = "ml-artifacts" +backend_type = "s3" +backend_prefix = "v2" # Prepend "v2/" to all keys when forwarding +anonymous_access = false +allowed_roles = ["github-actions-deployer"] + +[buckets.backend_options] +endpoint = "https://minio.internal:9000" +bucket_name = "ml-pipeline-artifacts" +region = "us-east-1" +access_key_id = "minioadmin" +secret_access_key = "minioadmin" + +# An S3 bucket on a different region +[[buckets]] +name = "deploy-bundles" +backend_type = "s3" +anonymous_access = false +allowed_roles = ["github-actions-deployer", "ci-readonly"] + +[buckets.backend_options] +endpoint = "https://s3.us-west-2.amazonaws.com" +bucket_name = "prod-deploy-bundles" +region = "us-west-2" +access_key_id = "AKIAI44QH8DHBEXAMPLE" +secret_access_key = "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY" + +# An Azure Blob Storage backend (requires "azure" feature) +[[buckets]] +name = "azure-data" +backend_type = "az" +anonymous_access = true +allowed_roles = [] + +[buckets.backend_options] +account_name = "mystorageaccount" +container_name = "public-datasets" + +# ============================================================================= +# IAM Roles (for STS AssumeRoleWithWebIdentity) +# ============================================================================= + +# Role for GitHub Actions CI/CD pipelines +[[roles]] +role_id = "github-actions-deployer" # Used as RoleArn in STS requests +name = "GitHub Actions Deploy Role" +trusted_oidc_issuers = ["https://token.actions.githubusercontent.com"] +required_audience = "sts.s3proxy.example.com" # Token's `aud` must match + +# Glob patterns for the `sub` claim +subject_conditions = [ + "repo:myorg/myapp:ref:refs/heads/main", + "repo:myorg/myapp:ref:refs/heads/release/*", + "repo:myorg/infrastructure:*", +] +max_session_duration_secs = 3600 # 1 hour max + +# Scopes granted to minted credentials +[[roles.allowed_scopes]] +bucket = "ml-artifacts" +prefixes = ["models/", "datasets/"] # Restrict to these prefixes +actions = [ + "get_object", "head_object", "put_object", + "create_multipart_upload", "upload_part", "complete_multipart_upload" +] + +[[roles.allowed_scopes]] +bucket = "deploy-bundles" +prefixes = [] # Full bucket access +actions = [ + "get_object", "head_object", "put_object", + "create_multipart_upload", "upload_part", "complete_multipart_upload" +] + +# Role with template variables for per-user access +[[roles]] +role_id = "source-user" +name = "Source Cooperative User" +trusted_oidc_issuers = ["https://auth.source.coop", "https://auth.staging.source.coop"] +subject_conditions = ["*"] # Any subject +max_session_duration_secs = 3600 + +# {sub} is replaced with the JWT's `sub` claim at mint time +[[roles.allowed_scopes]] +bucket = "{sub}" +prefixes = [] +actions = ["get_object", "head_object", "put_object", "list_bucket"] + +# Read-only role for CI +[[roles]] +role_id = "ci-readonly" +name = "CI Read-Only Role" +trusted_oidc_issuers = ["https://token.actions.githubusercontent.com"] +subject_conditions = ["repo:myorg/*"] # Any repo in the org +max_session_duration_secs = 1800 # 30 minutes + +[[roles.allowed_scopes]] +bucket = "deploy-bundles" +prefixes = [] +actions = ["get_object", "head_object", "list_bucket"] + +# ============================================================================= +# Long-Lived Credentials +# ============================================================================= + +# Service account for an internal tool +[[credentials]] +access_key_id = "AKPROXY00000EXAMPLE" +secret_access_key = "proxy/secret/key/EXAMPLE000000000000" +principal_name = "internal-dashboard" +created_at = "2024-01-15T00:00:00Z" +enabled = true # Set to false to revoke + +[[credentials.allowed_scopes]] +bucket = "public-data" +prefixes = [] +actions = ["get_object", "head_object", "list_bucket"] + +[[credentials.allowed_scopes]] +bucket = "ml-artifacts" +prefixes = ["models/production/"] +actions = ["get_object", "head_object"] +``` + +## Environment Variables + +These are set separately from the config file: + +```bash +# Required for STS temporary credentials (sealed tokens) +export SESSION_TOKEN_KEY=$(openssl rand -base64 32) + +# Required for OIDC backend auth +export OIDC_PROVIDER_KEY=$(cat oidc-key.pem) +export OIDC_PROVIDER_ISSUER="https://data.source.coop" + +# Logging +export RUST_LOG="source_coop=info" +``` diff --git a/docs/reference/errors.md b/docs/reference/errors.md new file mode 100644 index 0000000..569fe9e --- /dev/null +++ b/docs/reference/errors.md @@ -0,0 +1,58 @@ +# Error Codes + +The proxy returns S3-compatible error responses in XML format: + +```xml + + AccessDenied + Access Denied + 550e8400-e29b-41d4-a716-446655440000 + +``` + +## Error Types + +| Error | HTTP Status | S3 Code | When | +|-------|------------|---------|------| +| BucketNotFound | 404 | `NoSuchBucket` | Requested bucket doesn't exist in config | +| NoSuchKey | 404 | `NoSuchKey` | Key not found in backend (forwarded from backend response) | +| AccessDenied | 403 | `AccessDenied` | Caller lacks permission for the requested operation | +| SignatureDoesNotMatch | 403 | `SignatureDoesNotMatch` | SigV4 signature verification failed | +| MissingAuth | 403 | `AccessDenied` | Authentication required but no credentials provided | +| ExpiredCredentials | 403 | `ExpiredToken` | Temporary credentials have expired | +| InvalidOidcToken | 400 | `InvalidIdentityToken` | JWT validation failed (bad signature, untrusted issuer, etc.) | +| RoleNotFound | 403 | `AccessDenied` | Requested role doesn't exist in config | +| InvalidRequest | 400 | `InvalidRequest` | Malformed S3 request | +| BackendError | 503 | `ServiceUnavailable` | Backend object store is unreachable or returned an error | +| PreconditionFailed | 412 | `PreconditionFailed` | Conditional request failed (If-Match, etc.) | +| NotModified | 304 | `NotModified` | Conditional request — content not changed | +| ConfigError | 500 | `InternalError` | Invalid proxy configuration | +| Internal | 500 | `InternalError` | Unexpected internal error | + +## STS Error Responses + +STS errors follow the AWS STS error format: + +```xml + + + InvalidIdentityToken + Token signature verification failed + + 550e8400-e29b-41d4-a716-446655440000 + +``` + +| HTTP Status | Code | When | +|------------|------|------| +| 400 | `MalformedPolicyDocument` | Role not found in config | +| 400 | `InvalidIdentityToken` | JWT invalid, untrusted issuer, algorithm unsupported, subject mismatch | +| 400 | `InvalidParameterValue` | Missing required STS parameters | +| 403 | `AccessDenied` | General authorization failure | +| 500 | `InternalError` | Unexpected error during token exchange | + +## Error Message Safety + +For 5xx errors, the proxy returns generic messages to avoid leaking internal infrastructure details. The full error message is logged server-side but not exposed to clients. + +For 4xx errors, the proxy returns descriptive messages to help clients debug authentication and authorization issues. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..0ea31a6 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,5 @@ +# Reference + +- [Supported Operations](./operations) — S3 operations the proxy handles, including dispatch method +- [Error Codes](./errors) — Error types, S3 error codes, and HTTP status codes +- [Config Example](./config-example) — Annotated full configuration file diff --git a/docs/reference/operations.md b/docs/reference/operations.md new file mode 100644 index 0000000..09388e8 --- /dev/null +++ b/docs/reference/operations.md @@ -0,0 +1,43 @@ +# Supported Operations + +## S3 Operations + +| Operation | HTTP Method | Dispatch | Description | +|-----------|------------|----------|-------------| +| GetObject | `GET /{bucket}/{key}` | Forward | Download a file | +| HeadObject | `HEAD /{bucket}/{key}` | Forward | Get file metadata | +| PutObject | `PUT /{bucket}/{key}` | Forward | Upload a file | +| DeleteObject | `DELETE /{bucket}/{key}` | Forward | Delete a file | +| ListBucket | `GET /{bucket}` | Response | List objects in a bucket (ListObjectsV2) | +| ListBuckets | `GET /` | Response | List all virtual buckets | +| CreateMultipartUpload | `POST /{bucket}/{key}?uploads` | NeedsBody | Initiate a multipart upload | +| UploadPart | `PUT /{bucket}/{key}?partNumber=N&uploadId=ID` | NeedsBody | Upload a part | +| CompleteMultipartUpload | `POST /{bucket}/{key}?uploadId=ID` | NeedsBody | Complete a multipart upload | +| AbortMultipartUpload | `DELETE /{bucket}/{key}?uploadId=ID` | NeedsBody | Abort a multipart upload | + +### Dispatch Types + +- **Forward** — A presigned URL is generated and returned to the runtime, which executes it with its native HTTP client. Bodies stream directly between client and backend without buffering. +- **Response** — The handler builds a complete response (XML for LIST, error responses) and returns it. No presigned URL involved. +- **NeedsBody** — The runtime collects the request body, then the handler signs and sends the request via raw HTTP (`backend.send_raw()`). Multipart only. + +## STS Operations + +| Operation | HTTP Method | Description | +|-----------|------------|-------------| +| AssumeRoleWithWebIdentity | `POST /?Action=AssumeRoleWithWebIdentity&...` | Exchange OIDC JWT for temporary credentials | + +## OIDC Discovery Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/.well-known/openid-configuration` | GET | OpenID Connect discovery document | +| `/.well-known/jwks.json` | GET | JSON Web Key Set (proxy's RSA public key) | + +These are served when `OIDC_PROVIDER_KEY` and `OIDC_PROVIDER_ISSUER` are configured. + +## Limitations + +- **LIST returns all results** — `object_store::list_with_delimiter()` fetches all pages internally. `IsTruncated` is always `false`. Continuation tokens and max-keys are not supported. +- **Multipart is S3 only** — Multipart operations use raw HTTP with `S3RequestSigner` and are gated to `backend_type = "s3"`. Non-S3 backends should use single PUT requests. +- **DeleteObject does not return confirmation** — The proxy forwards the DELETE and returns the backend's response status. From 233940a159a83eb935da8d85a0f54461aca7c8ca Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sun, 1 Mar 2026 11:02:19 -0800 Subject: [PATCH 2/4] feat: add endpoints guide and fix mermaid diagram line breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add user-facing Endpoints page documenting global (data.source.coop) vs regional (us-west-2.data.source.coop) proxy endpoints, with guidance on when to use each for throughput and egress cost savings. Fix mermaid diagrams using literal \n for line breaks — replace with
tags for proper rendering. Co-Authored-By: Claude Opus 4.6 --- docs/.vitepress/config.ts | 1 + docs/architecture/index.md | 14 ++--- docs/auth/index.md | 4 +- docs/configuration/index.md | 6 +-- docs/guide/endpoints.md | 102 ++++++++++++++++++++++++++++++++++++ docs/guide/index.md | 5 +- docs/index.md | 10 ++-- 7 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 docs/guide/endpoints.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bf4dfb5..bd1c24a 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -115,6 +115,7 @@ export default withMermaid( text: "User Guide", items: [ { text: "Overview", link: "/guide/" }, + { text: "Endpoints", link: "/guide/endpoints" }, { text: "Authentication", link: "/guide/authentication" }, { text: "Client Usage", link: "/guide/client-usage" }, ], diff --git a/docs/architecture/index.md b/docs/architecture/index.md index 652f564..652ccb0 100644 --- a/docs/architecture/index.md +++ b/docs/architecture/index.md @@ -6,17 +6,17 @@ The Source Data Proxy is an S3-compliant gateway that sits between clients and b ```mermaid flowchart LR - Clients["S3 Clients\n(aws-cli, boto3, SDKs)"] + Clients["S3 Clients
(aws-cli, boto3, SDKs)"] subgraph Proxy["source-coop-proxy"] - Resolver["Request Resolver\n(parse, auth, authorize)"] - Handler["Proxy Handler\n(dispatch operations)"] - Backend["Proxy Backend\n(runtime-specific I/O)"] + Resolver["Request Resolver
(parse, auth, authorize)"] + Handler["Proxy Handler
(dispatch operations)"] + Backend["Proxy Backend
(runtime-specific I/O)"] end - Config["Config Provider\n(Static, HTTP, DynamoDB, Postgres)"] - OIDC["OIDC Providers\n(Auth0, GitHub, Keycloak)"] - Stores["Object Stores\n(S3, MinIO, R2, Azure, GCS)"] + Config["Config Provider
(Static, HTTP, DynamoDB, Postgres)"] + OIDC["OIDC Providers
(Auth0, GitHub, Keycloak)"] + Stores["Object Stores
(S3, MinIO, R2, Azure, GCS)"] Clients <--> Resolver Resolver <--> Config diff --git a/docs/auth/index.md b/docs/auth/index.md index c68523f..213ed78 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -9,10 +9,10 @@ The Source Data Proxy has two distinct authentication concerns: flowchart LR Client["S3 Client"] Proxy["Data Proxy"] - Backend["Object Store\n(S3, Azure, GCS)"] + Backend["Object Store
(S3, Azure, GCS)"] Client -- "SigV4, STS/OIDC" --> Proxy - Proxy -- "Presigned URLs,\nOIDC Exchange,\nor Static Credentials" --> Backend + Proxy -- "Presigned URLs,
OIDC Exchange,
or Static Credentials" --> Backend ``` ## Client Authentication diff --git a/docs/configuration/index.md b/docs/configuration/index.md index cd7afb0..293ef76 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -9,9 +9,9 @@ The proxy configuration defines three things: ```mermaid flowchart TD Config["Proxy Configuration"] - Config --> Buckets["Buckets\n(virtual names → backends)"] - Config --> Roles["Roles\n(OIDC trust policies)"] - Config --> Creds["Credentials\n(static access keys)"] + Config --> Buckets["Buckets
(virtual names → backends)"] + Config --> Roles["Roles
(OIDC trust policies)"] + Config --> Creds["Credentials
(static access keys)"] Roles -- "allowed_scopes" --> Buckets Creds -- "allowed_scopes" --> Buckets diff --git a/docs/guide/endpoints.md b/docs/guide/endpoints.md new file mode 100644 index 0000000..1d38c1a --- /dev/null +++ b/docs/guide/endpoints.md @@ -0,0 +1,102 @@ +# Endpoints + +Source Cooperative runs two types of proxy deployments. Choosing the right endpoint can significantly improve throughput and reduce costs. + +## Global Endpoint (Cloudflare Workers) + +``` +https://data.source.coop +``` + +The primary endpoint runs on Cloudflare Workers at the edge. This is the default for most use cases: + +- **Global availability** — Requests are handled by the nearest Cloudflare edge location +- **Backbone routing** — Traffic between Cloudflare and AWS travels over Cloudflare's private backbone network rather than the public internet, improving throughput and reliability +- **Best for** — Workstations, laptops, CI/CD outside AWS, or any client not running inside an AWS region + +```bash +aws s3 cp s3://my-bucket/data.parquet ./data.parquet \ + --endpoint-url https://data.source.coop +``` + +## Regional Endpoints (AWS Servers) + +``` +https://{region}.data.source.coop +``` + +For workloads running inside AWS, zone-specific server deployments are available. These run as native Tokio/Hyper servers within the same AWS region as the backend storage: + +| Endpoint | Region | +|----------|--------| +| `us-west-2.data.source.coop` | US West (Oregon) | +| `us-east-1.data.source.coop` | US East (N. Virginia) | + +Regional endpoints provide two major advantages: + +- **Higher throughput** — Traffic stays within the AWS network, avoiding internet bottlenecks. This is especially impactful for large file transfers and batch processing workloads +- **No egress fees** — Data transferred between S3 and an EC2 instance (or other AWS service) in the same region incurs no AWS data transfer charges. Using the global endpoint from within AWS would route traffic out through Cloudflare and back, incurring egress fees on both legs + +### When to Use Regional Endpoints + +Use a regional endpoint when your client is running inside the same AWS region as the data: + +- **EC2 instances** processing datasets stored in the same region +- **SageMaker notebooks** or training jobs accessing training data +- **Lambda functions** reading/writing data in batch pipelines +- **ECS/EKS workloads** performing ETL or analytics +- **AWS Batch** jobs processing large datasets + +```bash +# From an EC2 instance in us-west-2 +aws s3 cp s3://my-bucket/large-dataset.parquet ./data.parquet \ + --endpoint-url https://us-west-2.data.source.coop +``` + +### AWS Profile Configuration + +Set up profiles for both global and regional access: + +```ini +# For general use (laptop, CI/CD outside AWS) +[profile source-coop] +credential_process = source-coop credential-process +endpoint_url = https://data.source.coop + +# For workloads in us-west-2 +[profile source-coop-usw2] +credential_process = source-coop credential-process +endpoint_url = https://us-west-2.data.source.coop +``` + +```bash +# From your laptop +aws s3 ls s3://my-bucket/ --profile source-coop + +# From an EC2 instance in us-west-2 +aws s3 ls s3://my-bucket/ --profile source-coop-usw2 +``` + +## Choosing an Endpoint + +```mermaid +flowchart TD + Start["Where is your client running?"] + Start -->|Inside AWS| Region["Same region as the data?"] + Start -->|Outside AWS| Global["Use data.source.coop"] + + Region -->|Yes| Regional["Use {region}.data.source.coop"] + Region -->|No / Unsure| Global +``` + +| Scenario | Recommended Endpoint | Why | +|----------|---------------------|-----| +| Laptop or workstation | `data.source.coop` | Cloudflare backbone optimizes global routing | +| GitHub Actions / CI | `data.source.coop` | CI runners are typically outside AWS | +| EC2 in us-west-2, data in us-west-2 | `us-west-2.data.source.coop` | Same-region: max throughput, zero egress | +| EC2 in us-east-1, data in us-west-2 | `data.source.coop` | Cross-region: Cloudflare backbone is faster than cross-region AWS traffic | +| SageMaker in us-west-2 | `us-west-2.data.source.coop` | Same-region: zero egress for training data | + +::: tip +All endpoints support the same authentication methods and S3 operations. Your credentials work across any endpoint — only the `endpoint_url` changes. +::: diff --git a/docs/guide/index.md b/docs/guide/index.md index 6d390fa..3766cc7 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -4,8 +4,9 @@ The Source Data Proxy provides S3-compatible access to data stored across multip ## Getting Started -1. **[Authentication](./authentication)** — How to authenticate and obtain credentials -2. **[Client Usage](./client-usage)** — Using aws-cli, boto3, curl, and other S3 clients +1. **[Endpoints](./endpoints)** — Which endpoint to use (global vs. regional) +2. **[Authentication](./authentication)** — How to authenticate and obtain credentials +3. **[Client Usage](./client-usage)** — Using aws-cli, boto3, curl, and other S3 clients ## Quick Example diff --git a/docs/index.md b/docs/index.md index 7b30baf..cd6f317 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,15 +35,15 @@ features: ```mermaid flowchart LR - Clients["S3 Clients\n(aws-cli, boto3, SDKs)"] + Clients["S3 Clients
(aws-cli, boto3, SDKs)"] subgraph Proxy["source-coop-proxy"] - Auth["Auth\n(STS, OIDC, SigV4)"] - Core["Core\n(Proxy Handler)"] - Config["Config\n(Static, HTTP, DynamoDB, Postgres)"] + Auth["Auth
(STS, OIDC, SigV4)"] + Core["Core
(Proxy Handler)"] + Config["Config
(Static, HTTP, DynamoDB, Postgres)"] end - Backend["Backend Stores\n(AWS S3, MinIO, R2, Azure, GCS)"] + Backend["Backend Stores
(AWS S3, MinIO, R2, Azure, GCS)"] Clients <--> Proxy Proxy <--> Backend From 68749093a4f44287d3d77792c8c0d337cbeabba4 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sun, 1 Mar 2026 14:36:03 -0800 Subject: [PATCH 3/4] feat: add "Why a Proxy?" section and update homepage feature cards Add a narrative section to the homepage explaining the five core motivations: unified interface across backends, native S3 compatibility, metered access for sustainable open data, flexible OIDC auth on both frontend and backend, and multi-runtime deployment. Update feature cards to lead with user-facing value (Unified Interface, Native S3 Compatibility, Metered Access, Flexible Auth) rather than implementation details. Co-Authored-By: Claude Opus 4.6 --- docs/index.md | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/docs/index.md b/docs/index.md index cd6f317..ed07d26 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,20 +17,47 @@ hero: link: https://github.com/source-cooperative/data.source.coop features: + - title: Unified Interface + details: One stable URL per dataset, regardless of which object storage provider hosts the bytes. Backend migrations are invisible to data consumers. + - title: Native S3 Compatibility + details: Works with aws-cli, boto3, DuckDB, the object_store crate, GDAL, and any S3-compatible client. No custom SDK — just set the endpoint URL. + - title: Metered Access + details: Enforce per-identity rate limits so open data stays free for humans while protecting infrastructure from runaway machine access and egress costs. + - title: Flexible Auth + details: OIDC token exchange for both frontend (user/machine identity) and backend (cloud storage credentials). No long-lived keys anywhere in the chain. - title: Multi-Runtime - details: Deploy as a native Tokio/Hyper server in containers, or as a Cloudflare Worker at the edge. Same core logic, different runtimes. - - title: Multi-Provider - details: Proxy to AWS S3, MinIO, Cloudflare R2, Azure Blob Storage, or Google Cloud Storage through a unified S3-compatible API. - - title: OIDC/STS Authentication - details: Exchange OIDC tokens from any identity provider (GitHub Actions, Auth0, Keycloak, Cognito, Ory) for scoped temporary credentials via AssumeRoleWithWebIdentity. + details: Same core logic deploys as a native Tokio/Hyper server in containers or as a Cloudflare Worker at the edge. - title: Zero-Copy Streaming details: Presigned URLs enable direct streaming between clients and backends. No buffering, no double-handling of request or response bodies. - - title: Modular Architecture - details: Compose your own proxy with pluggable traits for backends, config providers, and request resolvers. Use the defaults or bring your own. - - title: OIDC Backend Auth - details: The proxy acts as its own OIDC identity provider to authenticate with cloud backends — no long-lived credentials needed. --- +## Why a Proxy? + +Source Cooperative hosts open data from researchers and organizations around the world. That data lives on object storage — but object storage alone doesn't solve the problems that come with making data truly accessible. + +### One URL, any backend + +A dataset might start on AWS S3, move to Cloudflare R2 to reduce egress costs, or get mirrored across providers for redundancy. The proxy gives every data product a stable URL (`data.source.coop/{account}/{dataset}/...`) regardless of where the bytes actually live. Backend migrations are invisible to consumers — no broken links, no client reconfiguration. + +### Native S3 compatibility + +Rather than inventing a new API, the proxy speaks the S3 protocol. This means the entire ecosystem of existing tools — `aws-cli`, `boto3`, DuckDB, the Rust `object_store` crate, GDAL, and hundreds of others — works out of the box. Users don't install a custom client or learn a new SDK. They just set an endpoint URL. + +### Metered access + +Open data should be free and open to humans, but without guardrails a single runaway script can rack up thousands of dollars in egress charges. The proxy enables metered access — enforcing limits on how much data a given identity can consume in a window of time. Public datasets stay freely accessible while the infrastructure stays sustainable. + +### Flexible authentication + +The proxy supports two layers of OIDC-based auth that eliminate long-lived credentials: + +- **Frontend**: Third-party identity providers (GitHub Actions, Auth0, Keycloak) can exchange OIDC tokens for scoped, time-limited proxy credentials — enabling machine-to-machine workflows like ETL pipelines and CI/CD without sharing static keys. +- **Backend**: The proxy acts as its own OIDC identity provider to authenticate with cloud storage backends, replacing long-lived access keys with short-lived credentials obtained via token exchange. + +### Run anywhere + +The same core logic compiles to a native Tokio/Hyper server for container deployments and to WebAssembly for Cloudflare Workers at the edge. Choose the runtime that fits your infrastructure — or run both. + ## How It Works ```mermaid From 929cf9e3c3f7412fcbd2aeb8656bd08edc2dda31 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Sun, 1 Mar 2026 14:38:02 -0800 Subject: [PATCH 4/4] ci: add GitHub Actions workflow to deploy docs to GitHub Pages Builds the VitePress site and deploys to GitHub Pages on pushes to main that touch docs/ or the workflow file. Supports manual dispatch via workflow_dispatch. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/docs.yaml | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/docs.yaml diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..20c2462 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,59 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/docs.yaml" + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: docs/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: docs + + - name: Build docs + run: pnpm docs:build + working-directory: docs + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + name: Deploy + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/deploy-pages@v4 + id: deployment