diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 2cef49a4f..d8d74706e 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -18,7 +18,7 @@ on: catalog_base_url: description: "Catalog base URL for immutable artifact links" required: false - default: "https://corvus.profiletailors.com" + default: "https://plugins.corvus.profiletailors.com" oci_repository: description: "OCI repository (example: ghcr.io/org/corvus-plugins/memory-surreal-graphs)" required: false @@ -80,7 +80,10 @@ jobs: input_plugin_id = os.environ.get("INPUT_PLUGIN_ID", "").strip() input_plugin_version = os.environ.get("INPUT_PLUGIN_VERSION", "").strip() input_catalog_base_url = ( - os.environ.get("INPUT_CATALOG_BASE_URL", "https://corvus.profiletailors.com") + os.environ.get( + "INPUT_CATALOG_BASE_URL", + "https://plugins.corvus.profiletailors.com", + ) .strip() .rstrip("/") ) @@ -91,7 +94,7 @@ jobs: effective_cf_project_name = input_cf_project_name or default_cf_project_name if not input_catalog_base_url: - input_catalog_base_url = "https://corvus.profiletailors.com" + input_catalog_base_url = "https://plugins.corvus.profiletailors.com" if not input_deploy_cf: input_deploy_cf = "true" diff --git a/clients/agent-runtime/src/config/schema.rs b/clients/agent-runtime/src/config/schema.rs index 8151acbc9..d13826d58 100644 --- a/clients/agent-runtime/src/config/schema.rs +++ b/clients/agent-runtime/src/config/schema.rs @@ -1023,7 +1023,7 @@ fn default_plugin_revocation_refresh_minutes() -> u64 { } fn default_plugin_revocation_sources() -> Vec { - vec!["https://corvus.profiletailors.com/revocations.json".to_string()] + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] } impl Default for PluginRevocationConfig { @@ -1067,7 +1067,7 @@ fn default_plugin_allow_publishers() -> Vec { fn default_plugin_sources() -> Vec { vec![PluginSourceConfig { name: "official".to_string(), - url: "https://corvus.profiletailors.com/catalog.json".to_string(), + url: "https://plugins.corvus.profiletailors.com/catalog.json".to_string(), plugin_identity_regex: None, }] } @@ -2220,12 +2220,20 @@ fn env_override_optional(var_name: &str, target: &mut Option) { fn migrate_deprecated_plugin_registry_urls(config: &mut Config) -> bool { const OLD_PLUGIN_HOST: &str = "plugins.corvus.ai"; - const NEW_PLUGIN_HOST: &str = "corvus.profiletailors.com"; + const LEGACY_PLUGIN_HOST: &str = "corvus.profiletailors.com"; + const NEW_PLUGIN_HOST: &str = "plugins.corvus.profiletailors.com"; - fn migrate_url_host(raw_url: &str, old_host: &str, new_host: &str) -> Option { + fn host_matches(candidate: &str, host: &str) -> bool { + candidate == host || candidate.ends_with(&format!(".{host}")) + } + + fn migrate_url_host(raw_url: &str, old_hosts: &[&str], new_host: &str) -> Option { let mut parsed = Url::parse(raw_url).ok()?; let host = parsed.host_str()?; - if host == old_host || host.ends_with(&format!(".{old_host}")) { + if old_hosts + .iter() + .any(|old_host| host_matches(host, old_host)) + { parsed.set_host(Some(new_host)).ok()?; return Some(parsed.to_string()); } @@ -2234,18 +2242,17 @@ fn migrate_deprecated_plugin_registry_urls(config: &mut Config) -> bool { let mut changed = false; let mut revocation_changed = false; + let old_hosts = [OLD_PLUGIN_HOST, LEGACY_PLUGIN_HOST]; for source in &mut config.plugins.sources { - if let Some(migrated) = migrate_url_host(&source.url, OLD_PLUGIN_HOST, NEW_PLUGIN_HOST) { + if let Some(migrated) = migrate_url_host(&source.url, &old_hosts, NEW_PLUGIN_HOST) { source.url = migrated; changed = true; } } for source_url in &mut config.plugins.revocation.source_urls { - if let Some(migrated) = - migrate_url_host(source_url.as_str(), OLD_PLUGIN_HOST, NEW_PLUGIN_HOST) - { + if let Some(migrated) = migrate_url_host(source_url.as_str(), &old_hosts, NEW_PLUGIN_HOST) { *source_url = migrated; changed = true; revocation_changed = true; @@ -2336,8 +2343,9 @@ impl Config { if migrate_deprecated_plugin_registry_urls(&mut config) { tracing::warn!( - "Migrated deprecated plugin registry host entries from 'plugins.corvus.ai' \ - to 'corvus.profiletailors.com' in plugins.sources and \ + "Migrated deprecated plugin registry host entries from \ + 'plugins.corvus.ai' and 'corvus.profiletailors.com' to \ + 'plugins.corvus.profiletailors.com' in plugins.sources and \ plugins.revocation.source_urls" ); if let Err(error) = config.save() { @@ -2903,19 +2911,19 @@ default_temperature = 0.7 assert!(!plugins.sources.is_empty()); assert_eq!( plugins.sources[0].url, - "https://corvus.profiletailors.com/catalog.json" + "https://plugins.corvus.profiletailors.com/catalog.json" ); assert_eq!( plugins.revocation.source_urls, - vec!["https://corvus.profiletailors.com/revocations.json".to_string()] + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] ); assert_eq!( default_plugin_sources()[0].url, - "https://corvus.profiletailors.com/catalog.json" + "https://plugins.corvus.profiletailors.com/catalog.json" ); assert_eq!( default_plugin_revocation_sources(), - vec!["https://corvus.profiletailors.com/revocations.json".to_string()] + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] ); assert!(plugins .allow_publishers @@ -2939,14 +2947,14 @@ default_temperature = 0.7 ]; config.plugins.revocation.source_urls = vec![ "https://plugins.corvus.ai/revocations.json".to_string(), - "https://corvus.profiletailors.com/revocations.json".to_string(), + "https://plugins.corvus.profiletailors.com/revocations.json".to_string(), ]; let changed = migrate_deprecated_plugin_registry_urls(&mut config); assert!(changed); assert_eq!( config.plugins.sources[0].url, - "https://corvus.profiletailors.com/catalog.json" + "https://plugins.corvus.profiletailors.com/catalog.json" ); assert_eq!( config.plugins.sources[1].url, @@ -2954,7 +2962,7 @@ default_temperature = 0.7 ); assert_eq!( config.plugins.revocation.source_urls, - vec!["https://corvus.profiletailors.com/revocations.json".to_string()] + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] ); } @@ -2965,11 +2973,34 @@ default_temperature = 0.7 assert!(!changed); assert_eq!( config.plugins.sources[0].url, - "https://corvus.profiletailors.com/catalog.json" + "https://plugins.corvus.profiletailors.com/catalog.json" + ); + assert_eq!( + config.plugins.revocation.source_urls, + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] + ); + } + + #[test] + fn migrate_deprecated_plugin_registry_urls_rewrites_legacy_profiletailors_host() { + let mut config = Config::default(); + config.plugins.sources = vec![PluginSourceConfig { + name: "official".to_string(), + url: "https://corvus.profiletailors.com/catalog.json".to_string(), + plugin_identity_regex: None, + }]; + config.plugins.revocation.source_urls = + vec!["https://corvus.profiletailors.com/revocations.json".to_string()]; + + let changed = migrate_deprecated_plugin_registry_urls(&mut config); + assert!(changed); + assert_eq!( + config.plugins.sources[0].url, + "https://plugins.corvus.profiletailors.com/catalog.json" ); assert_eq!( config.plugins.revocation.source_urls, - vec!["https://corvus.profiletailors.com/revocations.json".to_string()] + vec!["https://plugins.corvus.profiletailors.com/revocations.json".to_string()] ); } diff --git a/clients/agent-runtime/src/plugins/mod.rs b/clients/agent-runtime/src/plugins/mod.rs index 237732843..4ca7a547a 100644 --- a/clients/agent-runtime/src/plugins/mod.rs +++ b/clients/agent-runtime/src/plugins/mod.rs @@ -22,7 +22,7 @@ const MAX_ARTIFACT_BYTES: usize = 50 * 1024 * 1024; const MAX_SIGNATURE_BYTES: usize = 64 * 1024; const MAX_CERTIFICATE_BYTES: usize = 512 * 1024; const COSIGN_VERIFY_TIMEOUT: Duration = Duration::from_secs(30); -const OFFICIAL_PLUGIN_CATALOG_HOST: &str = "corvus.profiletailors.com"; +const OFFICIAL_PLUGIN_CATALOG_HOST: &str = "plugins.corvus.profiletailors.com"; const SIGSTORE_GITHUB_OIDC_ISSUER: &str = "https://token.actions.githubusercontent.com"; const OFFICIAL_PLUGIN_IDENTITY_REGEX: &str = r"^https://github\.com/dallay/corvus/\.github/workflows/publish-plugins\.yml@.*$"; @@ -2065,7 +2065,7 @@ mod tests { fn signature_policy_requires_keyless_for_official_remote_source() { let source = PluginSourceConfig { name: "official".to_string(), - url: "https://corvus.profiletailors.com/catalog.json".to_string(), + url: "https://plugins.corvus.profiletailors.com/catalog.json".to_string(), plugin_identity_regex: None, }; diff --git a/clients/web/README.md b/clients/web/README.md index ef93fb9ab..762f78d1c 100644 --- a/clients/web/README.md +++ b/clients/web/README.md @@ -10,6 +10,7 @@ clients/web/ │ ├── docs/ # Documentation (Astro + Starlight) │ ├── marketing/ # Marketing landing and campaign pages (Astro) │ ├── plugins/ # Plugin catalog and revocations (Astro) +│ ├── plugins-edge/ # Plugin distribution API (Cloudflare Worker + R2) │ └── chat/ # ChatGPT-style conversational chat (Vue 3 + Vite) ├── packages/ │ └── shared/ # Shared utilities @@ -37,6 +38,12 @@ clients/web/ - URL configurable with `PLUGINS_URL` (dev default: `http://localhost:9990`) - Publishes official plugin metadata at `/catalog.json` and `/revocations.json` +### plugins-edge + +- Runtime metadata/artifact API served from Cloudflare Worker + R2 +- Intended production source for `/catalog.json`, `/revocations.json`, and `/artifacts/*` +- Default local port: 9797 + ### chat - Framework: Vue 3 + Vite + Tailwind + shadcn-vue style components @@ -61,6 +68,7 @@ pnpm build pnpm build:docs pnpm build:marketing pnpm build:plugins +pnpm build:plugins-edge pnpm build:chat # Compatibility (legacy alias) @@ -70,6 +78,7 @@ pnpm build:landing pnpm dev pnpm dev:marketing pnpm dev:plugins +pnpm dev:plugins-edge pnpm dev:chat # Compatibility (legacy alias) @@ -80,6 +89,9 @@ pnpm format pnpm check pnpm test pnpm test:chat + +# Deploy plugins edge worker +pnpm deploy:plugins-edge ``` ## Biome (Linter & Formatter) diff --git a/clients/web/apps/docs/src/content/docs/en/guides/plugins.md b/clients/web/apps/docs/src/content/docs/en/guides/plugins.md index 2e70c98d1..913e49f78 100644 --- a/clients/web/apps/docs/src/content/docs/en/guides/plugins.md +++ b/clients/web/apps/docs/src/content/docs/en/guides/plugins.md @@ -64,8 +64,8 @@ All new official plugins must satisfy: Relevant configuration defaults: -- Catalog: `https://corvus.profiletailors.com/catalog.json` -- Revocations: `https://corvus.profiletailors.com/revocations.json` +- Catalog: `https://plugins.corvus.profiletailors.com/catalog.json` +- Revocations: `https://plugins.corvus.profiletailors.com/revocations.json` See: @@ -212,4 +212,4 @@ Recommended production rollout: ### Migration from old plugin host On config load, Corvus migrates old `plugins.corvus.ai` host references to -`corvus.profiletailors.com` for both catalog and revocation source URLs. +`plugins.corvus.profiletailors.com` for both catalog and revocation source URLs. diff --git a/clients/web/apps/docs/src/content/docs/es/guides/plugins.md b/clients/web/apps/docs/src/content/docs/es/guides/plugins.md index 7f37ca331..2b5f65438 100644 --- a/clients/web/apps/docs/src/content/docs/es/guides/plugins.md +++ b/clients/web/apps/docs/src/content/docs/es/guides/plugins.md @@ -65,8 +65,8 @@ Todos los plugins oficiales nuevos deben satisfacer: Configuración por defecto relevante: -- Catálogo: `https://corvus.profiletailors.com/catalog.json` -- Revocaciones: `https://corvus.profiletailors.com/revocations.json` +- Catálogo: `https://plugins.corvus.profiletailors.com/catalog.json` +- Revocaciones: `https://plugins.corvus.profiletailors.com/revocations.json` Ver: @@ -215,4 +215,4 @@ Rollout recomendado para producción: ### Migración del host de plugins antiguo Al cargar config, Corvus migra referencias antiguas del host `plugins.corvus.ai` a -`corvus.profiletailors.com` para ambas URLs de fuente de catálogo y revocación. +`plugins.corvus.profiletailors.com` para ambas URLs de fuente de catálogo y revocación. diff --git a/clients/web/apps/plugins-edge/.gitignore b/clients/web/apps/plugins-edge/.gitignore new file mode 100644 index 000000000..cfd84630a --- /dev/null +++ b/clients/web/apps/plugins-edge/.gitignore @@ -0,0 +1,3 @@ +.wrangler/ +dist/ +node_modules/ diff --git a/clients/web/apps/plugins-edge/README.md b/clients/web/apps/plugins-edge/README.md new file mode 100644 index 000000000..3cc509df7 --- /dev/null +++ b/clients/web/apps/plugins-edge/README.md @@ -0,0 +1,53 @@ +# Plugins Edge API (Cloudflare Worker + R2) + +This worker serves runtime plugin distribution assets directly from Cloudflare R2. + +It is designed to keep plugin installation metadata (`catalog.json`, `revocations.json`) +and immutable artifacts (`.wasm`, `.sig`, `.pem`) consistent and fast to fetch. + +## Endpoints + +- `GET /catalog.json` +- `GET /revocations.json` +- `GET /artifacts///` + +## Security Defaults + +- Rejects unsupported methods (`405`) +- Rejects malformed artifact paths (`400`) +- Rejects path traversal attempts (`..`, backslashes) +- Applies `X-Content-Type-Options: nosniff` +- Does not expose directory listing routes + +## Cache Strategy + +- `catalog.json`: `public, max-age=300, stale-while-revalidate=60` +- `revocations.json`: `no-store, max-age=0` +- `artifacts/*`: `public, max-age=31536000, immutable` + +## Local Development + +```bash +pnpm --filter @corvus/plugins-edge run dev +``` + +## Deploy + +```bash +pnpm --filter @corvus/plugins-edge run deploy +``` + +## Cloudflare Configuration + +1. Create R2 bucket (example: `corvus-plugins-catalog-prod`). +2. Bind R2 bucket to worker as `PLUGINS_BUCKET`. +3. Configure custom domain route for this worker (`plugins.corvus.profiletailors.com`). +4. Upload objects into R2 using these keys: + - `catalog/catalog.json` + - `catalog/revocations.json` + - `artifacts///.wasm` + - `artifacts///.wasm.sig` + - `artifacts///.wasm.pem` + +`CATALOG_OBJECT_KEY` and `REVOCATIONS_OBJECT_KEY` can override defaults via +`wrangler.toml` vars if needed. diff --git a/clients/web/apps/plugins-edge/package.json b/clients/web/apps/plugins-edge/package.json new file mode 100644 index 000000000..28fad1876 --- /dev/null +++ b/clients/web/apps/plugins-edge/package.json @@ -0,0 +1,19 @@ +{ + "name": "@corvus/plugins-edge", + "version": "0.1.8", + "private": true, + "description": "Corvus plugins catalog edge API (Cloudflare Worker + R2)", + "type": "module", + "scripts": { + "build": "tsc --noEmit && mkdir -p dist", + "dev": "wrangler dev --local --port 9797", + "deploy": "wrangler deploy", + "format": "biome format --write src package.json wrangler.toml tsconfig.json README.md", + "check": "biome check src package.json wrangler.toml tsconfig.json README.md", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "5.9.3", + "wrangler": "4.67.0" + } +} diff --git a/clients/web/apps/plugins-edge/src/index.ts b/clients/web/apps/plugins-edge/src/index.ts new file mode 100644 index 000000000..f02c85b53 --- /dev/null +++ b/clients/web/apps/plugins-edge/src/index.ts @@ -0,0 +1,238 @@ +interface R2Object { + body: ReadableStream | null; + size: number; + httpEtag?: string; + httpMetadata?: { + contentType?: string; + }; +} + +interface R2BucketLike { + get(key: string): Promise; +} + +interface Env { + PLUGINS_BUCKET: R2BucketLike; + CATALOG_OBJECT_KEY?: string; + REVOCATIONS_OBJECT_KEY?: string; +} + +const DEFAULT_CATALOG_KEY = "catalog/catalog.json"; +const DEFAULT_REVOCATIONS_KEY = "catalog/revocations.json"; +const ARTIFACTS_PREFIX = "/artifacts/"; + +const RESPONSE_HEADERS = { + nosniff: "X-Content-Type-Options", + cacheControl: "Cache-Control", + contentType: "Content-Type", + contentLength: "Content-Length", + etag: "ETag", + allowOrigin: "Access-Control-Allow-Origin", + allowMethods: "Access-Control-Allow-Methods", + allowHeaders: "Access-Control-Allow-Headers", +} as const; + +const CACHE_POLICIES = { + catalog: "public, max-age=300, stale-while-revalidate=60", + revocations: "no-store, max-age=0", + artifact: "public, max-age=31536000, immutable", +} as const; + +const ALLOW_METHODS_VALUE = "GET, HEAD, OPTIONS"; + +export default { + async fetch(request: Request, env: Env): Promise { + if (request.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: corsHeaders(), + }); + } + + if (!isSupportedMethod(request.method)) { + const headers = corsHeaders(); + headers.set("Allow", ALLOW_METHODS_VALUE); + return new Response("Method Not Allowed", { + status: 405, + headers, + }); + } + + const url = new URL(request.url); + const pathname = normalizePathname(url.pathname); + + if (pathname === "/catalog.json") { + return serveObject( + request, + env, + env.CATALOG_OBJECT_KEY || DEFAULT_CATALOG_KEY, + "application/json; charset=utf-8", + CACHE_POLICIES.catalog, + true + ); + } + + if (pathname === "/revocations.json") { + return serveObject( + request, + env, + env.REVOCATIONS_OBJECT_KEY || DEFAULT_REVOCATIONS_KEY, + "application/json; charset=utf-8", + CACHE_POLICIES.revocations, + true + ); + } + + if (pathname.startsWith(ARTIFACTS_PREFIX)) { + const key = safeArtifactKey(pathname); + if (!key) { + return jsonError(400, "Invalid artifact path"); + } + + return serveObject( + request, + env, + key, + guessArtifactContentType(key), + CACHE_POLICIES.artifact, + true + ); + } + + return jsonError(404, "Not found"); + }, +}; + +function isSupportedMethod(method: string): boolean { + return method === "GET" || method === "HEAD"; +} + +function normalizePathname(pathname: string): string { + if (!pathname || pathname === "/") { + return "/"; + } + + try { + const decoded = decodeURIComponent(pathname); + return decoded.replace(/\/{2,}/g, "/"); + } catch (error) { + if (error instanceof URIError) { + return pathname.replace(/\/{2,}/g, "/"); + } + throw error; + } +} + +function safeArtifactKey(pathname: string): string | null { + if (!pathname.startsWith(ARTIFACTS_PREFIX)) { + return null; + } + + if (pathname.includes("..") || pathname.includes("\\")) { + return null; + } + + const candidate = pathname.slice(1); + if (!candidate) { + return null; + } + + const valid = /^[a-zA-Z0-9._\-/]+$/.test(candidate); + if (!valid) { + return null; + } + + return candidate; +} + +async function serveObject( + request: Request, + env: Env, + key: string, + fallbackContentType: string, + cacheControl: string, + withCors: boolean +): Promise { + const object = await env.PLUGINS_BUCKET.get(key); + if (!object) { + return jsonError(404, "Not found"); + } + + const headers = new Headers(); + headers.set(RESPONSE_HEADERS.nosniff, "nosniff"); + headers.set(RESPONSE_HEADERS.cacheControl, cacheControl); + headers.set( + RESPONSE_HEADERS.contentType, + object.httpMetadata?.contentType || fallbackContentType + ); + headers.set(RESPONSE_HEADERS.contentLength, String(object.size)); + + if (object.httpEtag) { + headers.set(RESPONSE_HEADERS.etag, object.httpEtag); + } + + if (withCors) { + const cors = corsHeaders(); + cors.forEach((value, name) => { + headers.set(name, value); + }); + } + + const ifNoneMatch = request.headers.get("If-None-Match"); + if (ifNoneMatch && object.httpEtag && ifNoneMatch === object.httpEtag) { + const responseHeaders = new Headers(headers); + responseHeaders.delete(RESPONSE_HEADERS.contentLength); + responseHeaders.delete(RESPONSE_HEADERS.contentType); + return new Response(null, { + status: 304, + headers: responseHeaders, + }); + } + + if (request.method === "HEAD") { + return new Response(null, { + status: 200, + headers, + }); + } + + return new Response(object.body, { + status: 200, + headers, + }); +} + +function guessArtifactContentType(key: string): string { + if (key.endsWith(".wasm")) { + return "application/wasm"; + } + if (key.endsWith(".json")) { + return "application/json; charset=utf-8"; + } + if (key.endsWith(".pem")) { + return "application/x-pem-file"; + } + if (key.endsWith(".sig")) { + return "application/octet-stream"; + } + return "application/octet-stream"; +} + +function corsHeaders(): Headers { + const headers = new Headers(); + headers.set(RESPONSE_HEADERS.allowOrigin, "*"); + headers.set(RESPONSE_HEADERS.allowMethods, "GET, HEAD, OPTIONS"); + headers.set(RESPONSE_HEADERS.allowHeaders, "Content-Type, Authorization, If-None-Match"); + return headers; +} + +function jsonError(status: number, message: string): Response { + return new Response(JSON.stringify({ error: message }), { + status, + headers: { + [RESPONSE_HEADERS.contentType]: "application/json; charset=utf-8", + [RESPONSE_HEADERS.nosniff]: "nosniff", + [RESPONSE_HEADERS.cacheControl]: "no-store, max-age=0", + }, + }); +} diff --git a/clients/web/apps/plugins-edge/tsconfig.json b/clients/web/apps/plugins-edge/tsconfig.json new file mode 100644 index 000000000..0369fc434 --- /dev/null +++ b/clients/web/apps/plugins-edge/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "WebWorker"], + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/clients/web/apps/plugins-edge/wrangler.toml b/clients/web/apps/plugins-edge/wrangler.toml new file mode 100644 index 000000000..e2c5710f9 --- /dev/null +++ b/clients/web/apps/plugins-edge/wrangler.toml @@ -0,0 +1,12 @@ +name = "corvus-plugins-edge" +main = "src/index.ts" +compatibility_date = "2026-02-23" +workers_dev = true + +[vars] +CATALOG_OBJECT_KEY = "catalog/catalog.json" +REVOCATIONS_OBJECT_KEY = "catalog/revocations.json" + +[[r2_buckets]] +binding = "PLUGINS_BUCKET" +bucket_name = "corvus-plugins-catalog-prod" diff --git a/clients/web/apps/plugins/README.md b/clients/web/apps/plugins/README.md index 0178ff2fc..e57e7d6df 100644 --- a/clients/web/apps/plugins/README.md +++ b/clients/web/apps/plugins/README.md @@ -11,6 +11,9 @@ Dedicated site for publishing official runtime plugin metadata: Keep plugin metadata infrastructure separate from the marketing site so deployment, caching, and security controls remain independent. +Production runtime distribution is moving to `apps/plugins-edge` (Cloudflare Worker + R2) +to guarantee atomic publication of catalog and artifact assets. + ## Development ```bash @@ -27,7 +30,7 @@ pnpm --filter @corvus/plugins-catalog run check ## Domain and Port Configuration - Dev default: `http://localhost:9990` -- Prod default: `https://corvus.profiletailors.com` +- Prod default: `https://plugins.corvus.profiletailors.com` Supported variable: diff --git a/clients/web/build.gradle.kts b/clients/web/build.gradle.kts index bdb3e4ccf..98db2d914 100644 --- a/clients/web/build.gradle.kts +++ b/clients/web/build.gradle.kts @@ -73,6 +73,7 @@ val appConfigs = mapOf( "docs" to WebAppConfig("docs", "dist", 4321), "marketing" to WebAppConfig("marketing", "dist", 9988), "plugins" to WebAppConfig("plugins", "dist", 9990), + "plugins-edge" to WebAppConfig("plugins-edge", "dist", 9797), "chat" to WebAppConfig("chat", "dist", 4323), ) diff --git a/clients/web/package.json b/clients/web/package.json index 06ad99ccd..48bbb66b3 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -9,18 +9,21 @@ "build:docs": "pnpm --filter @corvus/docs run build", "build:marketing": "pnpm --filter @corvus/marketing run build", "build:plugins": "pnpm --filter @corvus/plugins-catalog run build", + "build:plugins-edge": "pnpm --filter @corvus/plugins-edge run typecheck", "build:landing": "pnpm run build:marketing", "build:chat": "pnpm --filter @corvus/chat run build", "dev": "pnpm --filter @corvus/docs run dev", "dev:marketing": "pnpm --filter @corvus/marketing run dev", "dev:plugins": "pnpm --filter @corvus/plugins-catalog run dev", + "dev:plugins-edge": "pnpm --filter @corvus/plugins-edge run dev", "dev:landing": "pnpm run dev:marketing", "dev:chat": "pnpm --filter @corvus/chat run dev", "format": "pnpm -r run format", "check": "pnpm -r run check", "clean": "pnpm -r run clean", "test": "pnpm -r run test", - "test:chat": "pnpm --filter @corvus/chat run test" + "test:chat": "pnpm --filter @corvus/chat run test", + "deploy:plugins-edge": "pnpm --filter @corvus/plugins-edge run deploy" }, "devDependencies": { "@biomejs/biome": "2.4.2", diff --git a/clients/web/pnpm-lock.yaml b/clients/web/pnpm-lock.yaml index f8b81d049..7f6692460 100644 --- a/clients/web/pnpm-lock.yaml +++ b/clients/web/pnpm-lock.yaml @@ -133,6 +133,15 @@ importers: specifier: 6.4.1 version: 6.4.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + apps/plugins-edge: + devDependencies: + typescript: + specifier: 5.9.3 + version: 5.9.3 + wrangler: + specifier: 4.67.0 + version: 4.67.0 + packages/shared: {} packages: @@ -278,6 +287,53 @@ packages: resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} engines: {node: '>=18'} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.14.0': + resolution: {integrity: sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260218.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260219.0': + resolution: {integrity: sha512-k+xM+swQBQnkrvwobjRPxyeYwjLSludJusR0PqeHe+h6X9QIRGgw3s1AO38lXQsqzMSgG5709oOXSF19NKVVaQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + resolution: {integrity: sha512-EyfQdsG1KcIVAf4qndT00LZly7sLFm1VxMWHBvOFB/EVYF2sE5HZ0dPbe+yrax5p3eS0oLZthR8ynhz4UulMUQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260219.0': + resolution: {integrity: sha512-N0UHXILYYa6htFO/uC92uAqusvynbSbOcHcrVXMKqP9Jy7eqXGMovyKIrNgzYnKIszNB+0lfUYdGI3Wci07LuA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + resolution: {integrity: sha512-835pjQ9uuAtwPBOAkPf+oxH3mNE5mqWuE3H7hJsul7WZsRD2FDcariyoT2AW6xyOePILrn4uMnmG1KGc9m/8Pg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260219.0': + resolution: {integrity: sha512-i7qcuOsuAxqqn1n5Ar3Rh1dHUL9vNmpF9FcdMTT84jIrdm5UNrPZz5grJthPmpB9LTcreT9iiP6qKbzGjnCwPA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@ctrl/tinycolor@4.2.0': resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} engines: {node: '>=14'} @@ -818,6 +874,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -864,6 +923,15 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@qwik.dev/partytown@0.11.2': resolution: {integrity: sha512-795y49CqBiKiwKAD+QBZlzlqEK275hVcazZ7wBPSfgC23L+vWuA7PJmMpgxojOucZHzYi5rAAQ+IP1I3BKVZxw==} engines: {node: '>=18.0.0'} @@ -1040,6 +1108,13 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -1431,6 +1506,9 @@ packages: bcp-47@2.1.0: resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1679,6 +1757,9 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2243,6 +2324,11 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + miniflare@4.20260219.0: + resolution: {integrity: sha512-EIb5wXbWUnnC60XU2aiFOPNd4fgTXzECkwRSOXZ1vdcY9WZaEE9rVf+h+Apw+WkOHRkp3Dr9/ZhQ5y1R+9iZ4Q==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} @@ -2349,6 +2435,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2606,6 +2695,10 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + svgo@4.0.0: resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} engines: {node: '>=16'} @@ -2693,6 +2786,13 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -3064,6 +3164,21 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + workerd@1.20260219.0: + resolution: {integrity: sha512-l4U4iT5H8jNV6+EK23ExnUV2z6JvqQtQPrT8XCm4G8RpwC9EPpYTOO9s/ImMPJKe1WSbQUQoJ4k8Nd83fz8skQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.67.0: + resolution: {integrity: sha512-58OoVth7bqm0nqsRgcI67gHbpp0IfR1JIBqDY0XR1FzRu9Qkjn6v2iJAdFf82QcVBFhaMBYQi88WqYGswq5wlQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260219.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3076,6 +3191,18 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -3129,6 +3256,12 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -3354,6 +3487,33 @@ snapshots: dependencies: fontkitten: 1.0.2 + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260219.0 + + '@cloudflare/workerd-darwin-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260219.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@ctrl/tinycolor@4.2.0': {} '@emmetio/abbreviation@2.3.3': @@ -3703,6 +3863,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -3760,6 +3925,18 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@qwik.dev/partytown@0.11.2': dependencies: dotenv: 16.6.1 @@ -3882,6 +4059,10 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.14': {} + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4389,6 +4570,8 @@ snapshots: is-alphanumerical: 2.0.1 is-decimal: 2.0.1 + blake3-wasm@2.1.5: {} + boolbase@1.0.0: {} boxen@8.0.1: @@ -4604,6 +4787,8 @@ snapshots: entities@7.0.1: {} + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} esast-util-from-estree@2.0.0: @@ -5603,6 +5788,18 @@ snapshots: transitivePeerDependencies: - supports-color + miniflare@4.20260219.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260219.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@9.0.1: dependencies: brace-expansion: 2.0.2 @@ -5713,6 +5910,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -6084,6 +6283,8 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + supports-color@10.2.2: {} + svgo@4.0.0: dependencies: commander: 11.1.0 @@ -6146,6 +6347,12 @@ snapshots: undici-types@7.16.0: {} + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -6475,6 +6682,30 @@ snapshots: dependencies: string-width: 7.2.0 + workerd@1.20260219.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260219.0 + '@cloudflare/workerd-darwin-arm64': 1.20260219.0 + '@cloudflare/workerd-linux-64': 1.20260219.0 + '@cloudflare/workerd-linux-arm64': 1.20260219.0 + '@cloudflare/workerd-windows-64': 1.20260219.0 + + wrangler@4.67.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260219.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260219.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6493,6 +6724,8 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.2 + ws@8.18.0: {} + ws@8.19.0: {} xxhash-wasm@1.1.0: {} @@ -6538,6 +6771,19 @@ snapshots: yoctocolors@2.1.2: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76