diff --git a/.env.example b/.env.example index e3d29d43..e628173e 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,10 @@ JWT_SECRET= # set by generate-secrets.sh SUPABASE_PUBLISHABLE_KEY= # set by generate-secrets.sh (anon JWT) SUPABASE_SECRET_KEY= # set by generate-secrets.sh (service_role JWT) +# --- Backend secrets --------------------------------------------------------- +DOWNLOAD_SIGNING_SECRET= # set by generate-secrets.sh; HMAC for /download/:token +USER_API_KEYS_ENCRYPTION_KEY= # set by generate-secrets.sh; encrypts user-stored LLM keys at rest + # --- GoTrue (laptop defaults; flip to false + add SMTP for real email) ------- GOTRUE_MAILER_AUTOCONFIRM=true GOTRUE_DISABLE_SIGNUP=false @@ -29,6 +33,19 @@ GOTRUE_DISABLE_SIGNUP=false # --- LLM providers (set at least one) ---------------------------------------- ANTHROPIC_API_KEY= GEMINI_API_KEY= +OPENROUTER_API_KEY= + +# --- MCP Connectors (optional) ----------------------------------------------- +# Externally-reachable backend URL used by MCP OAuth 2.1 callbacks. The +# default works for laptop use; OAuth-based MCP servers will only complete +# the callback if this URL is reachable from the third-party MCP server +# (i.e. you've exposed Mike to the public internet over TLS). +BACKEND_PUBLIC_URL=http://localhost:80/backend +# Allow MCP server URLs that point at private/loopback IPs and single-label +# hostnames. Required for any laptop dev where you run an MCP server on +# localhost or as a docker service alongside Mike. In production-style +# deployments (real domain, public traffic), leave unset to default-deny. +MCP_ALLOW_PRIVATE_HOSTS=true # --- Garage ------------------------------------------------------------------ GARAGE_RPC_SECRET= # set by generate-secrets.sh diff --git a/backend/.env.example b/backend/.env.example index 1db370a9..7cef3071 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,8 +1,21 @@ PORT=3001 FRONTEND_URL=http://localhost:3000 + +# HMAC key used to sign /download/:token URLs. Required at startup. +# Generate with: openssl rand -hex 32 +# Use a dedicated secret distinct from SUPABASE_SECRET_KEY. +DOWNLOAD_SIGNING_SECRET=replace-with-a-random-32-byte-hex-string + +# Externally-reachable backend URL. Used to build the OAuth callback URL for +# MCP connectors. Defaults to http://localhost:${PORT} when unset. +BACKEND_PUBLIC_URL=http://localhost:3001 SUPABASE_URL=https://your-project.supabase.co SUPABASE_SECRET_KEY=your-supabase-service-role-key +# Symmetric key used to encrypt user-supplied LLM API keys at rest. +# Required at startup. Generate with: openssl rand -hex 32 +USER_API_KEYS_ENCRYPTION_KEY=replace-with-a-random-32-byte-hex-string + R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com R2_ACCESS_KEY_ID=your-r2-access-key R2_SECRET_ACCESS_KEY=your-r2-secret-key diff --git a/backend/migrations/000_one_shot_schema.sql b/backend/migrations/000_one_shot_schema.sql index 80d563af..03082ebc 100644 --- a/backend/migrations/000_one_shot_schema.sql +++ b/backend/migrations/000_one_shot_schema.sql @@ -20,6 +20,7 @@ create table if not exists public.user_profiles ( tabular_model text not null default 'gemini-3-flash-preview', claude_api_key text, gemini_api_key text, + openrouter_api_key text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); @@ -30,14 +31,7 @@ create index if not exists idx_user_profiles_user alter table public.user_profiles enable row level security; drop policy if exists "Users can view their own profile" on public.user_profiles; -create policy "Users can view their own profile" - on public.user_profiles for select - using (auth.uid() = user_id); - drop policy if exists "Users can update their own profile" on public.user_profiles; -create policy "Users can update their own profile" - on public.user_profiles for update - using (auth.uid() = user_id); create or replace function public.handle_new_user() returns trigger @@ -82,6 +76,36 @@ create index if not exists idx_projects_user create index if not exists projects_shared_with_idx on public.projects using gin (shared_with); +alter table public.projects enable row level security; + +drop policy if exists projects_select_owner_or_shared on public.projects; +create policy projects_select_owner_or_shared + on public.projects for select + using ( + user_id = auth.uid()::text + or exists ( + select 1 + from jsonb_array_elements_text(coalesce(shared_with, '[]'::jsonb)) as member(email) + where lower(member.email) = lower(coalesce(auth.jwt()->>'email', '')) + ) + ); + +drop policy if exists projects_insert_owner_only on public.projects; +create policy projects_insert_owner_only + on public.projects for insert + with check (user_id = auth.uid()::text); + +drop policy if exists projects_update_owner_only on public.projects; +create policy projects_update_owner_only + on public.projects for update + using (user_id = auth.uid()::text) + with check (user_id = auth.uid()::text); + +drop policy if exists projects_delete_owner_only on public.projects; +create policy projects_delete_owner_only + on public.projects for delete + using (user_id = auth.uid()::text); + create table if not exists public.project_subfolders ( id uuid primary key default gen_random_uuid(), project_id uuid not null references public.projects(id) on delete cascade, @@ -242,6 +266,65 @@ create index if not exists idx_chats_user create index if not exists idx_chats_project on public.chats(project_id); +alter table public.chats enable row level security; + +drop policy if exists chats_select_owner_or_project_member on public.chats; +create policy chats_select_owner_or_project_member + on public.chats for select + using ( + user_id = auth.uid()::text + or ( + project_id is not null + and exists ( + select 1 + from public.projects p + where p.id = chats.project_id + and ( + p.user_id = auth.uid()::text + or exists ( + select 1 + from jsonb_array_elements_text(coalesce(p.shared_with, '[]'::jsonb)) as member(email) + where lower(member.email) = lower(coalesce(auth.jwt()->>'email', '')) + ) + ) + ) + ) + ); + +drop policy if exists chats_insert_user_and_project_access on public.chats; +create policy chats_insert_user_and_project_access + on public.chats for insert + with check ( + user_id = auth.uid()::text + and ( + project_id is null + or exists ( + select 1 + from public.projects p + where p.id = chats.project_id + and ( + p.user_id = auth.uid()::text + or exists ( + select 1 + from jsonb_array_elements_text(coalesce(p.shared_with, '[]'::jsonb)) as member(email) + where lower(member.email) = lower(coalesce(auth.jwt()->>'email', '')) + ) + ) + ) + ) + ); + +drop policy if exists chats_update_owner_only on public.chats; +create policy chats_update_owner_only + on public.chats for update + using (user_id = auth.uid()::text) + with check (user_id = auth.uid()::text); + +drop policy if exists chats_delete_owner_only on public.chats; +create policy chats_delete_owner_only + on public.chats for delete + using (user_id = auth.uid()::text); + create table if not exists public.chat_messages ( id uuid primary key default gen_random_uuid(), chat_id uuid not null references public.chats(id) on delete cascade, @@ -255,6 +338,68 @@ create table if not exists public.chat_messages ( create index if not exists idx_chat_messages_chat on public.chat_messages(chat_id); +alter table public.chat_messages enable row level security; + +drop policy if exists chat_messages_select_by_chat_access on public.chat_messages; +create policy chat_messages_select_by_chat_access + on public.chat_messages for select + using ( + exists ( + select 1 + from public.chats c + where c.id = chat_messages.chat_id + and ( + c.user_id = auth.uid()::text + or ( + c.project_id is not null + and exists ( + select 1 + from public.projects p + where p.id = c.project_id + and ( + p.user_id = auth.uid()::text + or exists ( + select 1 + from jsonb_array_elements_text(coalesce(p.shared_with, '[]'::jsonb)) as member(email) + where lower(member.email) = lower(coalesce(auth.jwt()->>'email', '')) + ) + ) + ) + ) + ) + ) + ); + +drop policy if exists chat_messages_insert_by_chat_access on public.chat_messages; +create policy chat_messages_insert_by_chat_access + on public.chat_messages for insert + with check ( + exists ( + select 1 + from public.chats c + where c.id = chat_messages.chat_id + and ( + c.user_id = auth.uid()::text + or ( + c.project_id is not null + and exists ( + select 1 + from public.projects p + where p.id = c.project_id + and ( + p.user_id = auth.uid()::text + or exists ( + select 1 + from jsonb_array_elements_text(coalesce(p.shared_with, '[]'::jsonb)) as member(email) + where lower(member.email) = lower(coalesce(auth.jwt()->>'email', '')) + ) + ) + ) + ) + ) + ) + ); + do $$ begin if not exists ( @@ -272,6 +417,56 @@ begin end; $$; +-- --------------------------------------------------------------------------- +-- User MCP servers +-- --------------------------------------------------------------------------- + +create table if not exists public.user_mcp_servers ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users(id) on delete cascade, + slug text not null, + name text not null, + url text not null, + headers jsonb not null default '{}'::jsonb, + enabled boolean not null default true, + last_error text, + auth_type text not null default 'headers' + check (auth_type in ('headers', 'oauth')), + oauth_metadata jsonb, + oauth_tokens jsonb, + oauth_code_verifier text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + constraint user_mcp_servers_slug_format + check (slug ~ '^[a-z0-9_-]{1,24}$'), + unique (user_id, slug) +); + +create index if not exists idx_user_mcp_servers_user + on public.user_mcp_servers(user_id, enabled); + +alter table public.user_mcp_servers enable row level security; + +drop policy if exists "Users can view their own MCP servers" on public.user_mcp_servers; +create policy "Users can view their own MCP servers" + on public.user_mcp_servers for select + using (auth.uid() = user_id); + +drop policy if exists "Users can insert their own MCP servers" on public.user_mcp_servers; +create policy "Users can insert their own MCP servers" + on public.user_mcp_servers for insert + with check (auth.uid() = user_id); + +drop policy if exists "Users can update their own MCP servers" on public.user_mcp_servers; +create policy "Users can update their own MCP servers" + on public.user_mcp_servers for update + using (auth.uid() = user_id); + +drop policy if exists "Users can delete their own MCP servers" on public.user_mcp_servers; +create policy "Users can delete their own MCP servers" + on public.user_mcp_servers for delete + using (auth.uid() = user_id); + -- --------------------------------------------------------------------------- -- Tabular reviews -- --------------------------------------------------------------------------- @@ -338,3 +533,45 @@ create table if not exists public.tabular_review_chat_messages ( create index if not exists tabular_review_chat_messages_chat_idx on public.tabular_review_chat_messages(chat_id, created_at); + +-- --------------------------------------------------------------------------- +-- Security posture +-- --------------------------------------------------------------------------- +-- App data is accessed through backend service-role routes. Keep RLS enabled +-- without direct anon/authenticated policies so browser clients cannot read or +-- write raw tables such as user profiles, document metadata, or API keys. + +alter table public.user_profiles enable row level security; +alter table public.projects enable row level security; +alter table public.project_subfolders enable row level security; +alter table public.documents enable row level security; +alter table public.document_versions enable row level security; +alter table public.document_edits enable row level security; +alter table public.workflows enable row level security; +alter table public.hidden_workflows enable row level security; +alter table public.workflow_shares enable row level security; +alter table public.chats enable row level security; +alter table public.chat_messages enable row level security; +alter table public.tabular_reviews enable row level security; +alter table public.tabular_cells enable row level security; +alter table public.tabular_review_chats enable row level security; +alter table public.tabular_review_chat_messages enable row level security; + +revoke all on public.user_profiles from anon, authenticated; +revoke all on public.projects from anon, authenticated; +revoke all on public.project_subfolders from anon, authenticated; +revoke all on public.documents from anon, authenticated; +revoke all on public.document_versions from anon, authenticated; +revoke all on public.document_edits from anon, authenticated; +revoke all on public.workflows from anon, authenticated; +revoke all on public.hidden_workflows from anon, authenticated; +revoke all on public.workflow_shares from anon, authenticated; +revoke all on public.chats from anon, authenticated; +revoke all on public.chat_messages from anon, authenticated; +revoke all on public.tabular_reviews from anon, authenticated; +revoke all on public.tabular_cells from anon, authenticated; +revoke all on public.tabular_review_chats from anon, authenticated; +revoke all on public.tabular_review_chat_messages from anon, authenticated; +-- user_mcp_servers carries OAuth tokens and Authorization headers; it +-- absolutely must not be reachable via PostgREST under anon/authenticated. +revoke all on public.user_mcp_servers from anon, authenticated; diff --git a/backend/migrations/001_add_openrouter_api_key.sql b/backend/migrations/001_add_openrouter_api_key.sql new file mode 100644 index 00000000..dcc33ef5 --- /dev/null +++ b/backend/migrations/001_add_openrouter_api_key.sql @@ -0,0 +1,5 @@ +-- Add OpenRouter API key column to user_profiles +-- Run this migration in your Supabase SQL Editor + +alter table public.user_profiles + add column if not exists openrouter_api_key text; diff --git a/backend/migrations/002_user_mcp_servers.sql b/backend/migrations/002_user_mcp_servers.sql new file mode 100644 index 00000000..bd592257 --- /dev/null +++ b/backend/migrations/002_user_mcp_servers.sql @@ -0,0 +1,49 @@ +-- User-configurable MCP (Model Context Protocol) servers. +-- Each row points the chat backend at a Streamable-HTTP MCP endpoint that +-- exposes additional tools. Tools discovered from these endpoints are merged +-- into the per-request tool list and routed under the `mcp____` prefix. +-- +-- Sensitive header values (e.g. Authorization tokens) live in the `headers` +-- jsonb column. Access is gated by Postgres RLS — owner-only — matching the +-- precedent set by user_profiles.claude_api_key / gemini_api_key. + +create table if not exists public.user_mcp_servers ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users(id) on delete cascade, + slug text not null, + name text not null, + url text not null, + headers jsonb not null default '{}'::jsonb, + enabled boolean not null default true, + last_error text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + constraint user_mcp_servers_slug_format + check (slug ~ '^[a-z0-9_-]{1,24}$'), + unique (user_id, slug) +); + +create index if not exists idx_user_mcp_servers_user + on public.user_mcp_servers(user_id, enabled); + +alter table public.user_mcp_servers enable row level security; + +drop policy if exists "Users can view their own MCP servers" on public.user_mcp_servers; +create policy "Users can view their own MCP servers" + on public.user_mcp_servers for select + using (auth.uid() = user_id); + +drop policy if exists "Users can insert their own MCP servers" on public.user_mcp_servers; +create policy "Users can insert their own MCP servers" + on public.user_mcp_servers for insert + with check (auth.uid() = user_id); + +drop policy if exists "Users can update their own MCP servers" on public.user_mcp_servers; +create policy "Users can update their own MCP servers" + on public.user_mcp_servers for update + using (auth.uid() = user_id); + +drop policy if exists "Users can delete their own MCP servers" on public.user_mcp_servers; +create policy "Users can delete their own MCP servers" + on public.user_mcp_servers for delete + using (auth.uid() = user_id); diff --git a/backend/migrations/003_user_mcp_servers_oauth.sql b/backend/migrations/003_user_mcp_servers_oauth.sql new file mode 100644 index 00000000..b53b75da --- /dev/null +++ b/backend/migrations/003_user_mcp_servers_oauth.sql @@ -0,0 +1,16 @@ +-- OAuth 2.1 support for user-configured MCP connectors. +-- Adds auth-mode toggle + storage for the discovered authorization-server +-- metadata, the dynamically-registered client info, the access/refresh +-- tokens, and the (transient) PKCE verifier between /oauth/start and +-- /oauth/callback. +-- +-- Tokens are stored at-rest in jsonb (RLS owner-only). Per-row encryption +-- is intentionally deferred to a separate hardening PR — this matches the +-- existing precedent for user_profiles.{claude,gemini}_api_key. + +alter table public.user_mcp_servers + add column if not exists auth_type text not null default 'headers' + check (auth_type in ('headers', 'oauth')), + add column if not exists oauth_metadata jsonb, + add column if not exists oauth_tokens jsonb, + add column if not exists oauth_code_verifier text; diff --git a/backend/migrations/004_security_lockdown.sql b/backend/migrations/004_security_lockdown.sql new file mode 100644 index 00000000..77074ec3 --- /dev/null +++ b/backend/migrations/004_security_lockdown.sql @@ -0,0 +1,67 @@ +-- Lock app data behind backend service-role APIs and clean up tabular cells +-- that point at documents outside their review authorization boundary. + +delete from public.tabular_cells c +where not exists ( + select 1 + from public.documents d + where d.id = c.document_id + ) + or exists ( + select 1 + from public.tabular_reviews r + left join public.documents d on d.id = c.document_id + where r.id = c.review_id + and ( + d.id is null + or ( + r.project_id is not null + and d.project_id is distinct from r.project_id + ) + or ( + r.project_id is null + and d.user_id is distinct from r.user_id + ) + ) + ); + +alter table public.user_profiles enable row level security; +alter table public.projects enable row level security; +alter table public.project_subfolders enable row level security; +alter table public.documents enable row level security; +alter table public.document_versions enable row level security; +alter table public.document_edits enable row level security; +alter table public.workflows enable row level security; +alter table public.hidden_workflows enable row level security; +alter table public.workflow_shares enable row level security; +alter table public.chats enable row level security; +alter table public.chat_messages enable row level security; +alter table public.tabular_reviews enable row level security; +alter table public.tabular_cells enable row level security; +alter table public.tabular_review_chats enable row level security; +alter table public.tabular_review_chat_messages enable row level security; +-- user_mcp_servers is RLS-on by default (per migration 002), but include +-- it here so the lockdown is explicit and re-runnable. +alter table public.user_mcp_servers enable row level security; + +drop policy if exists "Users can view their own profile" on public.user_profiles; +drop policy if exists "Users can update their own profile" on public.user_profiles; + +revoke all on public.user_profiles from anon, authenticated; +revoke all on public.projects from anon, authenticated; +revoke all on public.project_subfolders from anon, authenticated; +revoke all on public.documents from anon, authenticated; +revoke all on public.document_versions from anon, authenticated; +revoke all on public.document_edits from anon, authenticated; +revoke all on public.workflows from anon, authenticated; +revoke all on public.hidden_workflows from anon, authenticated; +revoke all on public.workflow_shares from anon, authenticated; +revoke all on public.chats from anon, authenticated; +revoke all on public.chat_messages from anon, authenticated; +revoke all on public.tabular_reviews from anon, authenticated; +revoke all on public.tabular_cells from anon, authenticated; +revoke all on public.tabular_review_chats from anon, authenticated; +revoke all on public.tabular_review_chat_messages from anon, authenticated; +-- user_mcp_servers carries OAuth tokens and Authorization headers; it +-- absolutely must not be reachable via PostgREST under anon/authenticated. +revoke all on public.user_mcp_servers from anon, authenticated; diff --git a/backend/package-lock.json b/backend/package-lock.json index 86f82382..c6f9ed0d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,17 +9,18 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { - "@anthropic-ai/sdk": "^0.90.0", - "@aws-sdk/client-s3": "^3.787.0", - "@aws-sdk/s3-request-presigner": "^3.787.0", + "@anthropic-ai/sdk": "^0.95.1", + "@aws-sdk/client-s3": "^3.1044.0", + "@aws-sdk/s3-request-presigner": "^3.1044.0", "@google/genai": "^1.50.1", + "@modelcontextprotocol/sdk": "^1.29.0", "@supabase/supabase-js": "^2.49.4", "cors": "^2.8.5", "docx": "^9.5.0", "dotenv": "^17.4.1", "express": "^4.21.2", "fast-diff": "^1.3.0", - "fast-xml-parser": "^5.7.1", + "fast-xml-parser": "^5.7.3", "jszip": "^3.10.1", "libreoffice-convert": "^1.6.0", "mammoth": "^1.9.0", @@ -38,12 +39,13 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.90.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", - "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", + "version": "0.95.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.1.tgz", + "integrity": "sha512-OO9AF7hmAoU492c/mD7Q2cPqI2WNAj7rAPHlawgBeUgpwiboLRiDs+grsErGWeHHP9ZRWfzq2OVrODTt8aITVg==", "license": "MIT", "dependencies": { - "json-schema-to-ts": "^3.1.1" + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" }, "bin": { "anthropic-ai-sdk": "bin/cli" @@ -260,65 +262,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1026.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1026.0.tgz", - "integrity": "sha512-tMP+s641FLSXdJazvYvuf38F7suWWv+wagTvShykPTffuFpBj5J9f7Rw0eKsauBcsjPSntiwBz9Gm0Tlh+cKfQ==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1045.0.tgz", + "integrity": "sha512-fsuO3Y6t+3Ro9Bsg41DKj4Sfy53CGSrhnMldNplWmG8Tx0UbYk+YDa4RD1hVlJpERw4JBmPkl0+J9qlxMh1pcA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/credential-provider-node": "^3.972.30", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.9", - "@aws-sdk/middleware-expect-continue": "^3.972.9", - "@aws-sdk/middleware-flexible-checksums": "^3.974.7", - "@aws-sdk/middleware-host-header": "^3.972.9", - "@aws-sdk/middleware-location-constraint": "^3.972.9", - "@aws-sdk/middleware-logger": "^3.972.9", - "@aws-sdk/middleware-recursion-detection": "^3.972.10", - "@aws-sdk/middleware-sdk-s3": "^3.972.28", - "@aws-sdk/middleware-ssec": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/region-config-resolver": "^3.972.11", - "@aws-sdk/signature-v4-multi-region": "^3.996.16", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@aws-sdk/util-user-agent-browser": "^3.972.9", - "@aws-sdk/util-user-agent-node": "^3.973.15", - "@smithy/config-resolver": "^4.4.14", - "@smithy/core": "^3.23.14", - "@smithy/eventstream-serde-browser": "^4.2.13", - "@smithy/eventstream-serde-config-resolver": "^4.3.13", - "@smithy/eventstream-serde-node": "^4.2.13", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/hash-blob-browser": "^4.2.14", - "@smithy/hash-node": "^4.2.13", - "@smithy/hash-stream-node": "^4.2.13", - "@smithy/invalid-dependency": "^4.2.13", - "@smithy/md5-js": "^4.2.13", - "@smithy/middleware-content-length": "^4.2.13", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-retry": "^4.5.0", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", + "@aws-sdk/middleware-expect-continue": "^3.972.10", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-location-constraint": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/middleware-ssec": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-blob-browser": "^4.2.15", + "@smithy/hash-node": "^4.2.14", + "@smithy/hash-stream-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/md5-js": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.45", - "@smithy/util-defaults-mode-node": "^4.2.49", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", - "@smithy/util-stream": "^4.5.22", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.15", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -326,22 +328,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", - "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/xml-builder": "^3.972.17", - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -350,12 +353,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.6.tgz", - "integrity": "sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz", + "integrity": "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -363,15 +366,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", - "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -379,20 +382,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", - "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-stream": "^4.5.22", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -400,24 +403,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", - "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-login": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -425,18 +428,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", - "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -444,22 +447,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", - "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-ini": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -467,16 +470,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", - "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -484,18 +487,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", - "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/token-providers": "3.1026.0", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -503,17 +506,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", - "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -521,16 +524,16 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.9.tgz", - "integrity": "sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz", + "integrity": "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", + "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -539,14 +542,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.9.tgz", - "integrity": "sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz", + "integrity": "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -554,23 +557,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.7.tgz", - "integrity": "sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==", + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/crc64-nvme": "^3.972.6", - "@aws-sdk/types": "^3.973.7", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/crc64-nvme": "^3.972.7", + "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-stream": "^4.5.22", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -579,14 +582,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", - "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -594,13 +597,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.9.tgz", - "integrity": "sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", + "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -608,13 +611,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", - "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -622,15 +625,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", - "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -638,23 +641,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.28.tgz", - "integrity": "sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-stream": "^4.5.22", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -663,13 +666,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.9.tgz", - "integrity": "sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", + "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -677,18 +680,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", - "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@smithy/core": "^3.23.14", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-retry": "^4.3.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" }, "engines": { @@ -696,47 +699,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", - "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/middleware-host-header": "^3.972.9", - "@aws-sdk/middleware-logger": "^3.972.9", - "@aws-sdk/middleware-recursion-detection": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/region-config-resolver": "^3.972.11", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@aws-sdk/util-user-agent-browser": "^3.972.9", - "@aws-sdk/util-user-agent-node": "^3.973.15", - "@smithy/config-resolver": "^4.4.14", - "@smithy/core": "^3.23.14", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/hash-node": "^4.2.13", - "@smithy/invalid-dependency": "^4.2.13", - "@smithy/middleware-content-length": "^4.2.13", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-retry": "^4.5.0", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.45", - "@smithy/util-defaults-mode-node": "^4.2.49", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -745,15 +749,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", - "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/config-resolver": "^4.4.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -761,18 +765,18 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1026.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1026.0.tgz", - "integrity": "sha512-PBVt/zb4YsJMcyB/HbGmID4RP00dTkdQGkNQiw1i6oXQ/U8hnPEI8+IvTKR4+5YEQ8Cq4QmtIV0mzv070L+oOg==", + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1045.0.tgz", + "integrity": "sha512-VDRF8GIuUPX+K4DUYrvcODj/h54LOmdJ7DhpLQ0wrYrdxzIiJEpi0n9jZ1bbjT2UxhwTbOorse5EGo+gnOK2aA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.16", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-format-url": "^3.972.9", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -780,16 +784,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.16.tgz", - "integrity": "sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==", + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.28", - "@aws-sdk/types": "^3.973.7", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -797,17 +801,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1026.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", - "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -815,12 +819,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", - "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -840,15 +844,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", - "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-endpoints": "^3.3.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" }, "engines": { @@ -856,14 +860,14 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz", - "integrity": "sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -883,27 +887,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", - "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", - "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/types": "^3.973.7", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -920,13 +924,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", - "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", - "fast-xml-parser": "5.5.8", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -934,9 +939,9 @@ } }, "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -945,9 +950,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -1414,9 +1420,10 @@ } }, "node_modules/@google/genai": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz", - "integrity": "sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "google-auth-library": "^10.3.0", @@ -1436,10 +1443,347 @@ } } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@napi-rs/canvas": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", - "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", "license": "MIT", "optional": true, "workspaces": [ @@ -1453,23 +1797,23 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.97", - "@napi-rs/canvas-darwin-arm64": "0.1.97", - "@napi-rs/canvas-darwin-x64": "0.1.97", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", - "@napi-rs/canvas-linux-arm64-musl": "0.1.97", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", - "@napi-rs/canvas-linux-x64-gnu": "0.1.97", - "@napi-rs/canvas-linux-x64-musl": "0.1.97", - "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", - "@napi-rs/canvas-win32-x64-msvc": "0.1.97" + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz", - "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", "cpu": [ "arm64" ], @@ -1487,9 +1831,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz", - "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", "cpu": [ "arm64" ], @@ -1507,9 +1851,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz", - "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", "cpu": [ "x64" ], @@ -1527,9 +1871,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz", - "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", "cpu": [ "arm" ], @@ -1547,9 +1891,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz", - "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", "cpu": [ "arm64" ], @@ -1567,9 +1911,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz", - "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", "cpu": [ "arm64" ], @@ -1587,9 +1931,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz", - "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", "cpu": [ "riscv64" ], @@ -1607,9 +1951,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz", - "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", "cpu": [ "x64" ], @@ -1627,9 +1971,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz", - "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", "cpu": [ "x64" ], @@ -1647,9 +1991,9 @@ } }, "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz", - "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", "cpu": [ "arm64" ], @@ -1667,9 +2011,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.97", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz", - "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==", + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", "cpu": [ "x64" ], @@ -1711,9 +2055,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { @@ -1739,9 +2083,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { @@ -1757,9 +2101,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, "node_modules/@react-email/render": { @@ -1819,16 +2163,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.14.tgz", - "integrity": "sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -1836,18 +2180,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.14", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", - "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-stream": "^4.5.22", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -1857,15 +2201,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", - "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -1873,13 +2217,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", - "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -1888,13 +2232,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", - "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1902,12 +2246,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", - "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1915,13 +2259,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", - "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1929,13 +2273,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", - "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1943,14 +2287,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", - "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -1959,14 +2303,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.14.tgz", - "integrity": "sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1974,12 +2318,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", - "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -1989,12 +2333,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.13.tgz", - "integrity": "sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2003,12 +2347,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", - "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2028,12 +2372,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.13.tgz", - "integrity": "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2042,13 +2386,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", - "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2056,18 +2400,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.29", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", - "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-middleware": "^4.2.13", + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -2075,19 +2419,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.0.tgz", - "integrity": "sha512-/NzISn4grj/BRFVua/xnQwF+7fakYZgimpw2dfmlPgcqecBMKxpB9g5mLYRrmBD5OrPoODokw4Vi1hrSR4zRyw==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/service-error-classification": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -2096,14 +2440,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", - "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2111,12 +2455,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", - "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2124,14 +2468,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", - "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2139,14 +2483,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", - "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2154,12 +2498,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", - "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2167,12 +2511,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", - "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2180,12 +2524,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", - "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -2194,12 +2538,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", - "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2207,24 +2551,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", - "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0" + "@smithy/types": "^4.14.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", - "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2232,16 +2576,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", - "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -2251,17 +2595,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", - "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-stream": "^4.5.22", + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -2269,9 +2613,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", - "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -2281,13 +2625,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", - "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2358,14 +2702,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", - "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2373,17 +2717,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.49", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.49.tgz", - "integrity": "sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==", + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.14", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2391,13 +2735,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.4.tgz", - "integrity": "sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2417,12 +2761,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", - "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2430,13 +2774,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.0.tgz", - "integrity": "sha512-tSOPQNT/4KfbvqeMovWC3g23KSYy8czHd3tlN+tOYVNIDLSfxIsrPJihYi5TpNcoV789KWtgChUVedh2y6dDPg==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2444,14 +2788,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.22", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", - "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/types": "^4.14.0", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -2488,12 +2832,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.15.tgz", - "integrity": "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2512,10 +2856,16 @@ "node": ">=18.0.0" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@supabase/auth-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.102.1.tgz", - "integrity": "sha512-2uH2WB0H98TOGDtaFWhxIcR42Dro/VB7VDZanz/4bVJsqioIue1m3TUqu3xciDm2W9r+1LXQvYNsYbQfWmD+uQ==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.3.tgz", + "integrity": "sha512-hMFuzP++mjRfe0/BUq4/e82CXIDgyjUgg0khLN8waol/gzoM1t2iGmhfJSGvQHQ1dr3XqWpP6ThAw4bLHMot5Q==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -2525,9 +2875,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.102.1.tgz", - "integrity": "sha512-UcrcKTPnAIo+Yp9Jjq9XXwFbsmgRYY637mwka9ZjmTIWcX/xr1pote4OVvaGQycVY1KTiQgjMvpC0Q0yJhRq3w==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.3.tgz", + "integrity": "sha512-KyutUwLLUZ9fRXsiFACL6lq7akBVHFl0fnqQnrxjbsPco8jeb4EyirQuvr52QCLnikzjMRC0uxAHOSM54aDrZA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -2537,15 +2887,15 @@ } }, "node_modules/@supabase/phoenix": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", - "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.1.tgz", + "integrity": "sha512-hWGJkDAfWUNY8k0C080u3sGNFd2ncl9erhKgP7hnGkgJWEfT5Pd/SXal4QmWXBECVlZrannMAc9sBaaRyWpiUA==", "license": "MIT" }, "node_modules/@supabase/postgrest-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.102.1.tgz", - "integrity": "sha512-InLvXKAYf8BIqiv9jWOYudWB3rU8A9uMbcip5BQ5sLLNPrbO1Ekkr79OvlhZBgMNSppxVyC7wPPGzLxMcTZhlA==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.3.tgz", + "integrity": "sha512-jFVYRHcri0ZMcTzKpQ2r2wWOB8/rPsbj92kxmCmVJUiRrdgiMtuYlkS06Fhs8UJZhEOL0UpGhh06XDwh8JwtBQ==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -2555,12 +2905,12 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.102.1.tgz", - "integrity": "sha512-h2fCumib/v6u7XMwSPgxnpfimjX4xCEayUHrxWLC7UurfQjUZJ0pmJDgm6yj80DnUerxuulRghwm5zXYysFG/Q==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.3.tgz", + "integrity": "sha512-L+qPiJlq1RKh3QD2fORGCFo2RKDKlvG9mjvPtUEQJ2tMixrx70VIV6j8BdWzQkbc1Nao6mvTWajyDhX3TFgljw==", "license": "MIT", "dependencies": { - "@supabase/phoenix": "^0.4.0", + "@supabase/phoenix": "^0.4.1", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" @@ -2570,9 +2920,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.102.1.tgz", - "integrity": "sha512-eCL9T4Xpe40nmKlkUJ7Zq/hk34db1xPiT0WL3Iv5MbJqHuCAe5TxhV8Rjqd6DNZrzjtfYObZtYl9jKJaHrivqw==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.3.tgz", + "integrity": "sha512-M7oPCCcHim/FsR6rKIs10Nd9mW051N2SQvA27jiVLa7oQMFFb7faX5dCQRV4GS5QeFsBcV5J/fWl4Ppoaw8cBQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -2583,16 +2933,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.102.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.102.1.tgz", - "integrity": "sha512-bChxPVeLDnYN9M2d/u4fXsvylwSQG5grAl+HN8f+ZD9a9PuVU+Ru+xGmEsk+b9Iz3rJC9ZQnQUJYQ28fApdWYA==", + "version": "2.105.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.3.tgz", + "integrity": "sha512-5Dm9+I61LAWwjw+0zcqXhSmTxUJaYHBPyHwMCIBH4TBUNwDn2pYUIsi6oUu0I5r9HtLtaFl7w4wa+DV9gRsbDg==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.102.1", - "@supabase/functions-js": "2.102.1", - "@supabase/postgrest-js": "2.102.1", - "@supabase/realtime-js": "2.102.1", - "@supabase/storage-js": "2.102.1" + "@supabase/auth-js": "2.105.3", + "@supabase/functions-js": "2.105.3", + "@supabase/postgrest-js": "2.105.3", + "@supabase/realtime-js": "2.105.3", + "@supabase/storage-js": "2.105.3" }, "engines": { "node": ">=20.0.0" @@ -2680,18 +3030,18 @@ } }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -2751,9 +3101,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -2781,6 +3131,39 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -2844,9 +3227,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -2857,7 +3240,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -2867,6 +3250,36 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -3008,6 +3421,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3078,18 +3505,18 @@ } }, "node_modules/docx/node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/docx/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/dom-serializer": { @@ -3148,9 +3575,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", - "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -3305,6 +3732,27 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -3351,6 +3799,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3358,9 +3824,9 @@ "license": "MIT" }, "node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-diff": { @@ -3369,10 +3835,32 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "license": "Apache-2.0" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -3385,9 +3873,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz", - "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", @@ -3397,7 +3885,7 @@ "license": "MIT", "dependencies": { "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", + "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, @@ -3566,9 +4054,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3639,9 +4127,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3650,6 +4138,15 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -3774,6 +4271,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3783,12 +4289,33 @@ "node": ">= 0.10" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -3811,6 +4338,18 @@ "node": ">=16" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4038,9 +4577,9 @@ } }, "node_modules/nanoid": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.7.tgz", - "integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", "funding": [ { "type": "github", @@ -4135,6 +4674,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", @@ -4206,6 +4754,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -4233,10 +4790,19 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -4255,22 +4821,22 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", + "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", + "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" }, @@ -4316,41 +4882,34 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", - "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" }, - "peerDependencies": { - "react": "^19.2.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/react-promise-suspense": { @@ -4362,6 +4921,12 @@ "fast-deep-equal": "^2.0.1" } }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -4383,6 +4948,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resend": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/resend/-/resend-4.8.0.tgz", @@ -4414,6 +4988,55 @@ "node": ">= 4" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4449,13 +5072,6 @@ "node": ">=11.0.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true - }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -4525,6 +5141,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -4545,13 +5182,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -4603,6 +5240,16 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4636,9 +5283,9 @@ "license": "MIT" }, "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -4784,6 +5431,27 @@ "node": ">= 8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -4840,6 +5508,24 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/backend/package.json b/backend/package.json index 50dfb585..37a0be7d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,20 +6,22 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", + "test": "node --import tsx --test test/**/*.test.ts", "start": "node dist/index.js" }, "dependencies": { - "@anthropic-ai/sdk": "^0.90.0", - "@aws-sdk/client-s3": "^3.787.0", - "@aws-sdk/s3-request-presigner": "^3.787.0", + "@anthropic-ai/sdk": "^0.95.1", + "@aws-sdk/client-s3": "^3.1044.0", + "@aws-sdk/s3-request-presigner": "^3.1044.0", "@google/genai": "^1.50.1", + "@modelcontextprotocol/sdk": "^1.29.0", "@supabase/supabase-js": "^2.49.4", "cors": "^2.8.5", "docx": "^9.5.0", "dotenv": "^17.4.1", "express": "^4.21.2", "fast-diff": "^1.3.0", - "fast-xml-parser": "^5.7.1", + "fast-xml-parser": "^5.7.3", "jszip": "^3.10.1", "libreoffice-convert": "^1.6.0", "mammoth": "^1.9.0", @@ -27,6 +29,9 @@ "pdfjs-dist": "^4.10.38", "resend": "^4.5.1" }, + "overrides": { + "@xmldom/xmldom": "0.8.13" + }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", diff --git a/backend/src/index.ts b/backend/src/index.ts index 0e99fffb..4f4d599a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -9,6 +9,8 @@ import { tabularRouter } from "./routes/tabular"; import { workflowsRouter } from "./routes/workflows"; import { userRouter } from "./routes/user"; import { downloadsRouter } from "./routes/downloads"; +import { mcpServersRouter } from "./routes/mcpServers"; +import { mcpOauthRouter } from "./routes/mcpOauth"; const app = express(); const PORT = process.env.PORT ?? 3001; @@ -31,6 +33,8 @@ app.use("/workflows", workflowsRouter); app.use("/user", userRouter); app.use("/users", userRouter); app.use("/download", downloadsRouter); +app.use("/user/mcp-servers", mcpServersRouter); +app.use("/mcp/oauth", mcpOauthRouter); app.get("/health", (_req, res) => res.json({ ok: true })); diff --git a/backend/src/lib/access.ts b/backend/src/lib/access.ts index a139888b..9f7e00e6 100644 --- a/backend/src/lib/access.ts +++ b/backend/src/lib/access.ts @@ -100,12 +100,16 @@ export async function ensureReviewAccess( userId: string, userEmail: string | null | undefined, db: Db, -): Promise<{ ok: true; isOwner: boolean } | { ok: false }> { - if (review.user_id === userId) return { ok: true, isOwner: true }; +): Promise< + | { ok: true; isOwner: boolean; via: "owner" | "project" | "direct" } + | { ok: false } +> { + if (review.user_id === userId) + return { ok: true, isOwner: true, via: "owner" }; const email = (userEmail ?? "").toLowerCase(); - if (email && Array.isArray(review.shared_with)) { + if (!review.project_id && email && Array.isArray(review.shared_with)) { if (review.shared_with.some((e) => (e ?? "").toLowerCase() === email)) { - return { ok: true, isOwner: false }; + return { ok: true, isOwner: false, via: "direct" }; } } if (!review.project_id) return { ok: false }; @@ -115,10 +119,16 @@ export async function ensureReviewAccess( userEmail, db, ); - if (access.ok) return { ok: true, isOwner: false }; + if (access.ok) return { ok: true, isOwner: false, via: "project" }; return { ok: false }; } +export function canEditReview( + access: { ok: true; isOwner: boolean; via?: "owner" | "project" | "direct" }, +): boolean { + return access.isOwner || access.via === "project"; +} + /** * Returns the set of project IDs the user can access — own projects plus * any project where their email is in `shared_with`. Used to scope chat @@ -129,13 +139,16 @@ export async function listAccessibleProjectIds( userEmail: string | null | undefined, db: Db, ): Promise { + // Stored shared_with values are lowercase; lowercase the JWT email too + // so providers that issue mixed-case email claims still match the row. + const email = userEmail?.toLowerCase(); const [{ data: own }, { data: shared }] = await Promise.all([ db.from("projects").select("id").eq("user_id", userId), - userEmail + email ? db .from("projects") .select("id") - .contains("shared_with", JSON.stringify([userEmail])) + .contains("shared_with", JSON.stringify([email])) .neq("user_id", userId) : Promise.resolve({ data: [] as { id: string }[] }), ]); diff --git a/backend/src/lib/apiKeys.ts b/backend/src/lib/apiKeys.ts new file mode 100644 index 00000000..1adff591 --- /dev/null +++ b/backend/src/lib/apiKeys.ts @@ -0,0 +1,179 @@ +import crypto from "crypto"; + +const ENCRYPTED_PREFIX = "enc:v1:"; + +function getEncryptionSecret(): string { + const secret = process.env.USER_API_KEYS_ENCRYPTION_KEY; + if (!secret?.trim()) { + throw new Error( + "USER_API_KEYS_ENCRYPTION_KEY is required to store user API keys", + ); + } + return secret.trim(); +} + +function keyFromSecret(secret: string): Buffer { + return crypto.createHash("sha256").update(secret, "utf8").digest(); +} + +export function isEncryptedApiKey(value: string | null | undefined): boolean { + return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX); +} + +export function encryptApiKey(value: string | null | undefined): string | null { + const plaintext = value?.trim(); + if (!plaintext) return null; + + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv( + "aes-256-gcm", + keyFromSecret(getEncryptionSecret()), + iv, + ); + const ciphertext = Buffer.concat([ + cipher.update(plaintext, "utf8"), + cipher.final(), + ]); + const tag = cipher.getAuthTag(); + return `${ENCRYPTED_PREFIX}${iv.toString("base64url")}.${tag.toString("base64url")}.${ciphertext.toString("base64url")}`; +} + +export function decryptApiKey(value: string | null | undefined): string | null { + if (!value) return null; + if (!isEncryptedApiKey(value)) { + // Legacy plaintext values are supported so existing deployments can + // continue while getUserApiKeys opportunistically rewrites them. + return value; + } + + const payload = value.slice(ENCRYPTED_PREFIX.length); + const [ivRaw, tagRaw, ciphertextRaw] = payload.split("."); + if (!ivRaw || !tagRaw || !ciphertextRaw) { + throw new Error("Stored API key has an invalid encrypted format"); + } + + const decipher = crypto.createDecipheriv( + "aes-256-gcm", + keyFromSecret(getEncryptionSecret()), + Buffer.from(ivRaw, "base64url"), + ); + decipher.setAuthTag(Buffer.from(tagRaw, "base64url")); + const plaintext = Buffer.concat([ + decipher.update(Buffer.from(ciphertextRaw, "base64url")), + decipher.final(), + ]); + return plaintext.toString("utf8"); +} + +export function hasStoredApiKey(value: string | null | undefined): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +/** The set of LLM provider keys we encrypt at rest. */ +export const PROVIDER_KEY_COLUMNS = [ + "claude_api_key", + "gemini_api_key", + "openrouter_api_key", +] as const; +export type ProviderKeyColumn = (typeof PROVIDER_KEY_COLUMNS)[number]; + +/** Maps the API names the frontend sends to the column names we store. */ +export const PROVIDER_KEY_COLUMN_BY_INPUT: Record = { + claude: "claude_api_key", + gemini: "gemini_api_key", + openrouter: "openrouter_api_key", +}; + +/** + * For each provider key column that is stored as plaintext, returns a record + * of `{ column: encryptedValue }` suitable for a database UPDATE (opportunistic + * upgrade path). Returns an empty object when no upgrades are needed. + */ +export function buildPlaintextUpgrades( + row: Partial>, +): Partial> { + const updates: Partial> = {}; + for (const col of PROVIDER_KEY_COLUMNS) { + const stored = row[col] ?? null; + if (stored && !isEncryptedApiKey(stored)) { + updates[col] = encryptApiKey(stored)!; + } + } + return updates; +} + +/** + * Converts a frontend `api_keys` payload (e.g. `{ claude: "sk-…" }`) into a + * record of encrypted column values ready to be merged into a database UPDATE. + */ +export function encryptApiKeyInputs( + apiKeys: Partial>, +): Partial> { + const updates: Partial> = {}; + for (const [input, col] of Object.entries(PROVIDER_KEY_COLUMN_BY_INPUT)) { + if (input in apiKeys) { + updates[col] = encryptApiKey(apiKeys[input]); + } + } + return updates; +} + +// --------------------------------------------------------------------------- +// JSON-blob helpers — used for MCP credentials (headers + oauth_tokens). +// +// Storage format: we JSON-serialize the value, encrypt the resulting string +// with the same AES-256-GCM envelope as the per-key helpers above, and write +// the ciphertext string into the jsonb column. A JSON string is itself valid +// jsonb, so this round-trips cleanly without needing a column type change. +// On read we sniff `typeof value === "string" && startsWith("enc:v1:")` to +// distinguish encrypted blobs from legacy plaintext objects. +// +// Encrypting the whole blob (rather than each leaf) keeps the format simple, +// minimizes cipher operations, and avoids leaking the shape (e.g. "this row +// has a refresh_token" vs "only an access_token") to anyone who can read the +// table. +// --------------------------------------------------------------------------- + +/** + * Encrypts an arbitrary JSON-serializable value to an `enc:v1:` ciphertext + * string suitable for storing in a jsonb column. Returns null for null/ + * undefined/empty inputs so callers can pass through "no value" cleanly. + */ +export function encryptJsonBlob(value: unknown): string | null { + if (value === null || value === undefined) return null; + const serialized = JSON.stringify(value); + if (!serialized) return null; + return encryptApiKey(serialized); +} + +/** + * Reverse of {@link encryptJsonBlob}. Accepts an encrypted ciphertext string, + * a legacy plaintext object/array (returned as-is), or null/undefined. Throws + * only if the value is an `enc:v1:` envelope but the ciphertext is malformed + * or the encryption key is wrong. + */ +export function decryptJsonBlob( + value: unknown, +): T | null { + if (value === null || value === undefined) return null; + if (typeof value === "string" && isEncryptedApiKey(value)) { + const plaintext = decryptApiKey(value); + if (plaintext === null) return null; + return JSON.parse(plaintext) as T; + } + // Legacy plaintext path: the column was written before encryption was + // enabled, so the jsonb already holds the structured value directly. + return value as T; +} + +/** + * True if the given jsonb-shaped value looks like a legacy plaintext blob + * that should be opportunistically re-encrypted. Strings that already carry + * the `enc:v1:` envelope are skipped; null/undefined are skipped; everything + * else (objects, arrays, plain strings without the envelope) is a candidate. + */ +export function needsJsonBlobUpgrade(value: unknown): boolean { + if (value === null || value === undefined) return false; + if (typeof value === "string" && isEncryptedApiKey(value)) return false; + return true; +} diff --git a/backend/src/lib/chatTools.ts b/backend/src/lib/chatTools.ts index c3ab2439..b3b6d00f 100644 --- a/backend/src/lib/chatTools.ts +++ b/backend/src/lib/chatTools.ts @@ -21,6 +21,8 @@ import { type LlmMessage, type OpenAIToolSchema, } from "./llm"; +import { findMcpServerForTool } from "./mcp/servers"; +import type { LoadedMcpServer } from "./mcp/types"; const STANDARD_FONT_DATA_URL = (() => { try { @@ -1140,18 +1142,10 @@ async function readDocumentContent( opts?: { emitEvents?: boolean }, ): Promise { const emitEvents = opts?.emitEvents ?? true; - console.log(`[read_document] called with docLabel="${docLabel}"`); const docInfo = docStore.get(docLabel); if (!docInfo) { - console.log( - `[read_document] MISS — docLabel "${docLabel}" not in docStore. Known labels:`, - Array.from(docStore.keys()), - ); return "Document not found."; } - console.log( - `[read_document] docInfo: filename="${docInfo.filename}", file_type="${docInfo.file_type}", storage_path="${docInfo.storage_path}"`, - ); const documentId = docIndex?.[docLabel]?.document_id; const emitDocRead = () => { @@ -1185,93 +1179,39 @@ async function readDocumentContent( current.bytes.byteOffset + current.bytes.byteLength, ) as ArrayBuffer; sourcePath = current.storage_path; - console.log( - `[read_document] using current version path="${sourcePath}" (bytes=${raw.byteLength})`, - ); - } else { - console.log( - `[read_document] loadCurrentVersionBytes returned null for documentId="${documentId}", falling back to original storage_path`, - ); } } if (!raw) { raw = await downloadFile(docInfo.storage_path); - if (raw) { - console.log( - `[read_document] fallback download from storage_path="${docInfo.storage_path}" (bytes=${raw.byteLength})`, - ); - } } if (!raw) { - console.log( - `[read_document] FAILED to download any bytes for docLabel="${docLabel}" (tried path="${sourcePath}")`, - ); emitDocRead(); return "Document could not be read."; } - // Log the first 8 bytes so we can identify real file format regardless - // of the declared file_type. Valid .docx starts with "PK\x03\x04" - // (zip). Legacy .doc starts with "\xD0\xCF\x11\xE0" (OLE/CFB). - // %PDF-1 is a PDF even if mislabeled. Truncated uploads show as all-zero. - { - const head = Buffer.from(raw).subarray(0, 8); - const hex = head.toString("hex"); - const ascii = head - .toString("binary") - .replace(/[^\x20-\x7e]/g, "."); - console.log( - `[read_document] magic bytes hex=${hex} ascii="${ascii}" for filename="${docInfo.filename}"`, - ); - } let text: string; if (docInfo.file_type === "pdf") { text = await extractPdfText(raw); - console.log( - `[read_document] pdf extracted length=${text.length} for filename="${docInfo.filename}"`, - ); } else if (docInfo.file_type === "docx") { // Use the same flattening as the edit_document matcher so the // LLM sees exactly the characters it can anchor against. text = await extractDocxBodyText(Buffer.from(raw)); - console.log( - `[read_document] docx extractDocxBodyText length=${text.length} for filename="${docInfo.filename}"`, - ); if (!text) { - console.log( - `[read_document] docx accepted-view extractor returned empty, falling back to mammoth for filename="${docInfo.filename}"`, - ); const mammoth = await import("mammoth"); const result = await mammoth.extractRawText({ buffer: Buffer.from(raw), }); text = result.value; - console.log( - `[read_document] docx mammoth fallback length=${text.length} for filename="${docInfo.filename}"`, - ); } } else { - console.log( - `[read_document] unknown file_type="${docInfo.file_type}" for filename="${docInfo.filename}", trying mammoth`, - ); const mammoth = await import("mammoth"); const result = await mammoth.extractRawText({ buffer: Buffer.from(raw), }); text = result.value; - console.log( - `[read_document] mammoth length=${text.length} for filename="${docInfo.filename}"`, - ); } - console.log( - `[read_document] DONE filename="${docInfo.filename}" finalTextLength=${text.length} firstChars=${JSON.stringify(text.slice(0, 120))}`, - ); emitDocRead(); return text; } catch (err) { - console.log( - `[read_document] THREW for docLabel="${docLabel}" filename="${docInfo.filename}":`, - err, - ); if (emitEvents) write(`data: ${JSON.stringify({ type: "doc_read", filename: docInfo.filename })}\n\n`); return "Document could not be read."; @@ -1484,6 +1424,32 @@ export type DocReplicatedResult = { }[]; }; +/** + * One MCP tool call worth of observability — surfaced to the chat UI so the + * user can see what was sent and what came back. `args` and `output` are + * already capped in size before this event is emitted/persisted. + */ +export type McpToolResultEvent = { + type: "mcp_tool_result"; + server: string; + tool: string; + ok: boolean; + args: string; + output: string; +}; + +/** + * Cap previewed args/output to keep `chat_messages.content` from bloating. + * The model still receives the full untruncated tool output — this only + * affects what is shown to and persisted for the user. + */ +const MCP_PREVIEW_MAX = 4096; + +function truncateForPreview(s: string): string { + if (s.length <= MCP_PREVIEW_MAX) return s; + return s.slice(0, MCP_PREVIEW_MAX) + "\n…(truncated)"; +} + export async function runToolCalls( toolCalls: ToolCall[], docStore: DocStore, @@ -1495,6 +1461,7 @@ export async function runToolCalls( docIndex?: DocIndex, turnEditState?: TurnEditState, projectId?: string | null, + mcpServers?: LoadedMcpServer[], ): Promise<{ toolResults: unknown[]; docsRead: { filename: string; document_id?: string }[]; @@ -1503,6 +1470,7 @@ export async function runToolCalls( docsReplicated: DocReplicatedResult[]; workflowsApplied: { workflow_id: string; title: string }[]; docsEdited: DocEditedResult[]; + mcpResults: McpToolResultEvent[]; }> { const toolResults: unknown[] = []; const docsRead: { filename: string; document_id?: string }[] = []; @@ -1515,6 +1483,7 @@ export async function runToolCalls( const docsReplicated: DocReplicatedResult[] = []; const workflowsApplied: { workflow_id: string; title: string }[] = []; const docsEdited: DocEditedResult[] = []; + const mcpResults: McpToolResultEvent[] = []; for (const tc of toolCalls) { let args: Record = {}; @@ -1524,6 +1493,49 @@ export async function runToolCalls( /* ignore */ } + if (tc.function.name.startsWith("mcp__") && mcpServers?.length) { + const match = findMcpServerForTool(tc.function.name, mcpServers); + if (!match) { + toolResults.push({ + role: "tool", + tool_call_id: tc.id, + content: `MCP tool '${tc.function.name}' not available (server may have been removed mid-request).`, + }); + continue; + } + const { server, originalName } = match; + write( + `data: ${JSON.stringify({ + type: "mcp_tool_call", + server: server.row.name, + tool: originalName, + })}\n\n`, + ); + const { ok, content } = await server.client.callTool( + originalName, + args, + ); + // The model already sees content capped at MCP_MAX_TOOL_BYTES + // (mcp/client.ts); the user-facing preview is further capped here + // to keep chat_messages.content from bloating. + const preview: McpToolResultEvent = { + type: "mcp_tool_result", + server: server.row.name, + tool: originalName, + ok, + args: truncateForPreview(JSON.stringify(args)), + output: truncateForPreview(content), + }; + write(`data: ${JSON.stringify(preview)}\n\n`); + mcpResults.push(preview); + toolResults.push({ + role: "tool", + tool_call_id: tc.id, + content, + }); + continue; + } + if (tc.function.name === "read_document") { const rawDocId = args.doc_id as string; const docId = @@ -2123,7 +2135,6 @@ export async function runToolCalls( } else if (tc.function.name === "generate_docx") { const title = args.title as string; const landscape = !!(args.landscape); - console.log(`[generate_docx] title="${title}" landscape=${landscape} args.landscape=${args.landscape}`); const previewFilename = `${(title.replace(/[^a-zA-Z0-9 _-]/g, "").trim().slice(0, 64) || "document")}.docx`; write(`data: ${JSON.stringify({ type: "doc_created_start", filename: previewFilename })}\n\n`); const result = await generateDocx( @@ -2206,6 +2217,7 @@ export async function runToolCalls( docsReplicated, workflowsApplied, docsEdited, + mcpResults, }; } @@ -2291,7 +2303,8 @@ type AssistantEvent = download_url: string; annotations: EditAnnotation[]; } - | { type: "content"; text: string }; + | { type: "content"; text: string } + | McpToolResultEvent; export async function runLLMStream(params: { apiMessages: unknown[]; @@ -2312,25 +2325,28 @@ export async function runLLMStream(params: { * generated docs still get persisted, but as standalone documents. */ projectId?: string | null; + /** + * MCP servers loaded for this user (already connected, with tool lists + * fetched). Their tools are merged into the per-request tool set under + * the `mcp____` prefix. Leave undefined when no MCP support is + * wired in or the user has none configured. + */ + mcpServers?: LoadedMcpServer[]; }): Promise<{ fullText: string; events: AssistantEvent[] }> { - const { apiMessages, docStore, docIndex, userId, db, write, extraTools, workflowStore, tabularStore, buildCitations, model, apiKeys, projectId } = params; - const activeTools = extraTools?.length - ? [...TOOLS, ...WORKFLOW_TOOLS, ...extraTools] - : [...TOOLS, ...WORKFLOW_TOOLS]; + const { apiMessages, docStore, docIndex, userId, db, write, extraTools, workflowStore, tabularStore, buildCitations, model, apiKeys, projectId, mcpServers } = params; + const mcpTools = (mcpServers ?? []).flatMap((s) => s.tools); + const activeTools = [ + ...TOOLS, + ...WORKFLOW_TOOLS, + ...(extraTools ?? []), + ...mcpTools, + ]; // Extract system prompt; pass remaining turns to the adapter as // plain user/assistant messages. const rawMsgs = apiMessages as { role: string; content: string | null }[]; const systemPrompt = rawMsgs[0]?.role === "system" ? (rawMsgs[0].content ?? "") : ""; - console.log( - "[runLLMStream] system prompt:\n" + - "─".repeat(80) + - "\n" + - systemPrompt + - "\n" + - "─".repeat(80), - ); const chatMessages: LlmMessage[] = rawMsgs .filter((m) => m.role !== "system") .map((m) => ({ @@ -2444,10 +2460,21 @@ export async function runLLMStream(params: { // and the first tool-specific event. onToolCallStart: (call) => { flushText(); + // For MCP tools, emit a friendly display name (server + tool) + // alongside the raw prefixed name. The UI renders display_name + // when present so users don't see `mcp____`. + let display_name: string | undefined; + if (call.name.startsWith("mcp__") && mcpServers?.length) { + const match = findMcpServerForTool(call.name, mcpServers); + if (match) { + display_name = `${match.server.row.name} · ${match.originalName}`; + } + } write( `data: ${JSON.stringify({ type: "tool_call_start", name: call.name, + ...(display_name ? { display_name } : {}), })}\n\n`, ); }, @@ -2472,6 +2499,7 @@ export async function runLLMStream(params: { docsReplicated, workflowsApplied, docsEdited, + mcpResults, } = await runToolCalls( toolCalls, docStore, @@ -2483,6 +2511,7 @@ export async function runLLMStream(params: { docIndex, turnEditState, projectId, + mcpServers, ); for (const r of docsRead) { events.push({ @@ -2535,6 +2564,9 @@ export async function runLLMStream(params: { annotations: e.annotations, }); } + for (const r of mcpResults) { + events.push(r); + } // Index alignment would break if any tool branch skips its // push (unhandled tool name, disabled store, guard failure). @@ -2698,14 +2730,6 @@ export async function buildDocContext( } } - console.log( - "[buildDocContext] available docs:", - Object.entries(docIndex).map(([label, info]) => ({ - label, - filename: info.filename, - document_id: info.document_id, - })), - ); return { docIndex, docStore }; } @@ -2776,15 +2800,6 @@ export async function buildProjectDocContext( if (path) folderPaths.set(docLabel, path); } - console.log( - "[buildProjectDocContext] available docs:", - Object.entries(docIndex).map(([label, info]) => ({ - label, - filename: info.filename, - document_id: info.document_id, - folder: folderPaths.get(label) ?? null, - })), - ); return { docIndex, docStore, folderPaths }; } diff --git a/backend/src/lib/downloadTokens.ts b/backend/src/lib/downloadTokens.ts index 0fe27cbc..1804d2f7 100644 --- a/backend/src/lib/downloadTokens.ts +++ b/backend/src/lib/downloadTokens.ts @@ -9,26 +9,21 @@ import crypto from "crypto"; * expiry or R2 CORS headaches. */ -function getSecret(): string { - return ( - process.env.DOWNLOAD_SIGNING_SECRET ?? - process.env.SUPABASE_SECRET_KEY ?? - "dev-secret" - ); -} - -function b64urlEncode(buf: Buffer): string { - return buf - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/g, ""); -} - -function b64urlDecode(s: string): Buffer { - let t = s.replace(/-/g, "+").replace(/_/g, "/"); - while (t.length % 4) t += "="; - return Buffer.from(t, "base64"); +/** + * Resolves the shared HMAC signing secret. Used here for download tokens + * and re-exported for other call sites that share the same threat model + * (e.g. MCP OAuth state tokens in lib/mcp/oauth.ts) so a single env var + * gates every signed token in the app. + */ +export function getSigningSecret(): string { + const secret = process.env.DOWNLOAD_SIGNING_SECRET; + if (!secret?.trim()) { + throw new Error( + "DOWNLOAD_SIGNING_SECRET is required. " + + "Generate a strong random value (e.g. `openssl rand -hex 32`) and set it in the environment.", + ); + } + return secret.trim(); } function timingSafeEqStr(a: string, b: string): boolean { @@ -38,12 +33,12 @@ function timingSafeEqStr(a: string, b: string): boolean { export function signDownload(path: string, filename: string): string { const payload = JSON.stringify({ p: path, f: filename }); - const enc = b64urlEncode(Buffer.from(payload, "utf8")); + const enc = Buffer.from(payload, "utf8").toString("base64url"); const sig = crypto - .createHmac("sha256", getSecret()) + .createHmac("sha256", getSigningSecret()) .update(enc) .digest(); - return `${enc}.${b64urlEncode(sig)}`; + return `${enc}.${sig.toString("base64url")}`; } export function verifyDownload( @@ -53,12 +48,12 @@ export function verifyDownload( if (parts.length !== 2) return null; const [enc, sigEnc] = parts; const expected = crypto - .createHmac("sha256", getSecret()) + .createHmac("sha256", getSigningSecret()) .update(enc) .digest(); - if (!timingSafeEqStr(sigEnc, b64urlEncode(expected))) return null; + if (!timingSafeEqStr(sigEnc, expected.toString("base64url"))) return null; try { - const parsed = JSON.parse(b64urlDecode(enc).toString("utf8")) as { + const parsed = JSON.parse(Buffer.from(enc, "base64url").toString("utf8")) as { p: string; f: string; }; diff --git a/backend/src/lib/llm/claude.ts b/backend/src/lib/llm/claude.ts index 9ed625eb..ee62de82 100644 --- a/backend/src/lib/llm/claude.ts +++ b/backend/src/lib/llm/claude.ts @@ -1,7 +1,5 @@ import Anthropic from "@anthropic-ai/sdk"; import type { Tool } from "@anthropic-ai/sdk/resources/messages/messages"; -import * as fs from "fs"; -import * as path from "path"; import type { StreamChatParams, StreamChatResult, @@ -10,10 +8,7 @@ import type { } from "./types"; import { toClaudeTools } from "./tools"; -const RAW_STREAM_LOG_PATH = path.resolve( - process.cwd(), - "claude-raw-stream.log", -); +const DEBUG_LLM_STREAM = process.env.DEBUG_LLM_STREAM === "true"; type ContentBlock = | { type: "text"; text: string } @@ -81,9 +76,9 @@ export async function streamClaude( let sawThinking = false; stream.on("streamEvent", (event) => { - const line = JSON.stringify(event); - console.log("[claude raw stream]", line); - fs.appendFile(RAW_STREAM_LOG_PATH, line + "\n", () => {}); + if (DEBUG_LLM_STREAM) { + console.debug("[claude raw stream]", JSON.stringify(event)); + } }); stream.on("text", (delta) => { diff --git a/backend/src/lib/llm/gemini.ts b/backend/src/lib/llm/gemini.ts index ee43d617..57e62d7e 100644 --- a/backend/src/lib/llm/gemini.ts +++ b/backend/src/lib/llm/gemini.ts @@ -77,7 +77,6 @@ export async function streamGemini( let sawThinking = false; for await (const chunk of stream) { - console.log("[gemini stream chunk]", JSON.stringify(chunk, null, 2)); const parts = (chunk as { candidates?: { content?: { parts?: GeminiPart[] } }[] }) .candidates?.[0]?.content?.parts ?? []; diff --git a/backend/src/lib/llm/index.ts b/backend/src/lib/llm/index.ts index 518ddc01..06e1bee6 100644 --- a/backend/src/lib/llm/index.ts +++ b/backend/src/lib/llm/index.ts @@ -1,5 +1,6 @@ import { streamClaude, completeClaudeText } from "./claude"; import { streamGemini, completeGeminiText } from "./gemini"; +import { streamOpenRouter, completeOpenRouterText } from "./openrouter"; import { providerForModel } from "./models"; import type { StreamChatParams, StreamChatResult, UserApiKeys } from "./types"; @@ -11,6 +12,7 @@ export async function streamChatWithTools( ): Promise { const provider = providerForModel(params.model); if (provider === "claude") return streamClaude(params); + if (provider === "openrouter") return streamOpenRouter(params); return streamGemini(params); } @@ -23,5 +25,6 @@ export async function completeText(params: { }): Promise { const provider = providerForModel(params.model); if (provider === "claude") return completeClaudeText(params); + if (provider === "openrouter") return completeOpenRouterText(params); return completeGeminiText(params); } diff --git a/backend/src/lib/llm/models.ts b/backend/src/lib/llm/models.ts index 52314007..b910555b 100644 --- a/backend/src/lib/llm/models.ts +++ b/backend/src/lib/llm/models.ts @@ -10,14 +10,25 @@ export const GEMINI_MAIN_MODELS = [ "gemini-3-flash-preview", ] as const; +// OpenRouter main-chat tier +export const OPENROUTER_MAIN_MODELS = [ + "openrouter/openai/gpt-5.3-chat", + "openrouter/anthropic/claude-sonnet-4-6", + "openrouter/anthropic/claude-opus-4-7", + "openrouter/x-ai/grok-4.3", + "openrouter/openai/gpt-4o-mini", +] as const; + // Mid-tier (used for tabular review) — user picks one in account settings. export const CLAUDE_MID_MODELS = ["claude-sonnet-4-6"] as const; export const GEMINI_MID_MODELS = ["gemini-3-flash-preview"] as const; +export const OPENROUTER_MID_MODELS = ["openrouter/openai/gpt-4o-mini"] as const; // Low-tier (used for title generation, lightweight extractions) — user picks // one in account settings. export const CLAUDE_LOW_MODELS = ["claude-haiku-4-5"] as const; export const GEMINI_LOW_MODELS = ["gemini-3.1-flash-lite-preview"] as const; +export const OPENROUTER_LOW_MODELS = ["openrouter/openai/gpt-4o-mini"] as const; export const DEFAULT_MAIN_MODEL = "gemini-3-flash-preview"; export const DEFAULT_TITLE_MODEL = "gemini-3.1-flash-lite-preview"; @@ -26,10 +37,13 @@ export const DEFAULT_TABULAR_MODEL = "gemini-3-flash-preview"; const ALL_MODELS = new Set([ ...CLAUDE_MAIN_MODELS, ...GEMINI_MAIN_MODELS, + ...OPENROUTER_MAIN_MODELS, ...CLAUDE_MID_MODELS, ...GEMINI_MID_MODELS, + ...OPENROUTER_MID_MODELS, ...CLAUDE_LOW_MODELS, ...GEMINI_LOW_MODELS, + ...OPENROUTER_LOW_MODELS, ]); // --------------------------------------------------------------------------- @@ -37,6 +51,7 @@ const ALL_MODELS = new Set([ // --------------------------------------------------------------------------- export function providerForModel(model: string): Provider { + if (model.startsWith("openrouter/")) return "openrouter"; if (model.startsWith("claude")) return "claude"; if (model.startsWith("gemini")) return "gemini"; throw new Error(`Unknown model id: ${model}`); diff --git a/backend/src/lib/llm/openrouter.ts b/backend/src/lib/llm/openrouter.ts new file mode 100644 index 00000000..1466298c --- /dev/null +++ b/backend/src/lib/llm/openrouter.ts @@ -0,0 +1,279 @@ +import type { + StreamChatParams, + StreamChatResult, + NormalizedToolCall, +} from "./types"; + +const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"; +const MAX_TOKENS = 16384; +const DEBUG_LLM_STREAM = process.env.DEBUG_LLM_STREAM === "true"; + +type OpenRouterMessage = { + role: "system" | "user" | "assistant" | "tool"; + content: string | null; + tool_calls?: { + id: string; + type: "function"; + function: { name: string; arguments: string }; + }[]; + tool_call_id?: string; +}; + +type OpenRouterChoice = { + delta?: { + content?: string | null; + tool_calls?: { + index: number; + id?: string; + type?: "function"; + function?: { name?: string; arguments?: string }; + }[]; + }; + finish_reason?: string | null; +}; + +type OpenRouterStreamChunk = { + choices: OpenRouterChoice[]; +}; + +function getApiKey(override?: string | null): string { + return override?.trim() || process.env.OPENROUTER_API_KEY || ""; +} + +/** + * Strip the "openrouter/" prefix from model IDs. + * e.g., "openrouter/openai/gpt-4o" -> "openai/gpt-4o" + */ +function toOpenRouterModelId(model: string): string { + return model.startsWith("openrouter/") ? model.slice("openrouter/".length) : model; +} + +function toOpenRouterMessages( + systemPrompt: string, + messages: StreamChatParams["messages"], +): OpenRouterMessage[] { + const result: OpenRouterMessage[] = []; + if (systemPrompt) { + result.push({ role: "system", content: systemPrompt }); + } + for (const m of messages) { + result.push({ role: m.role, content: m.content }); + } + return result; +} + +export async function streamOpenRouter( + params: StreamChatParams, +): Promise { + const { + model, + systemPrompt, + tools = [], + callbacks = {}, + runTools, + apiKeys, + } = params; + const maxIter = params.maxIterations ?? 10; + const apiKey = getApiKey(apiKeys?.openrouter); + const openRouterModel = toOpenRouterModelId(model); + + const messages: OpenRouterMessage[] = toOpenRouterMessages(systemPrompt, params.messages); + let fullText = ""; + + for (let iter = 0; iter < maxIter; iter++) { + const body: Record = { + model: openRouterModel, + messages, + max_tokens: MAX_TOKENS, + stream: true, + }; + + if (tools.length > 0) { + body.tools = tools; + body.tool_choice = "auto"; + } + + const response = await fetch(OPENROUTER_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "HTTP-Referer": process.env.APP_URL || "http://localhost:3000", + "X-Title": "Mike", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`); + } + + if (!response.body) { + throw new Error("OpenRouter response body is null"); + } + + // Parse SSE stream + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + // Per-iteration accumulators + const textParts: string[] = []; + const toolCalls: Map = new Map(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === "data: [DONE]") continue; + if (!trimmed.startsWith("data: ")) continue; + + const jsonStr = trimmed.slice(6); + let chunk: OpenRouterStreamChunk; + try { + chunk = JSON.parse(jsonStr); + } catch { + continue; + } + + if (DEBUG_LLM_STREAM) { + console.log("[openrouter stream chunk]", JSON.stringify(chunk, null, 2)); + } + + const choice = chunk.choices?.[0]; + if (!choice?.delta) continue; + + // Handle text content + if (choice.delta.content) { + textParts.push(choice.delta.content); + callbacks.onContentDelta?.(choice.delta.content); + } + + // Handle tool calls + if (choice.delta.tool_calls) { + for (const tc of choice.delta.tool_calls) { + const existing = toolCalls.get(tc.index); + if (existing) { + // Accumulate function arguments + if (tc.function?.arguments) { + existing.arguments += tc.function.arguments; + } + // Backfill id when first delta used a synthetic fallback + if (tc.id && (!existing.id || existing.id.startsWith("tool-"))) { + existing.id = tc.id; + } + } else { + // New tool call + toolCalls.set(tc.index, { + id: tc.id || `tool-${tc.index}`, + name: tc.function?.name || "", + arguments: tc.function?.arguments || "", + }); + } + } + } + } + } + + fullText += textParts.join(""); + + // Convert accumulated tool calls to normalized format + const normalizedCalls: NormalizedToolCall[] = []; + for (const [, tc] of toolCalls) { + if (!tc.name) continue; + let input: Record = {}; + try { + input = JSON.parse(tc.arguments || "{}"); + } catch { + input = {}; + } + const call: NormalizedToolCall = { + id: tc.id, + name: tc.name, + input, + }; + callbacks.onToolCallStart?.(call); + normalizedCalls.push(call); + } + + // If no tool calls or no runTools handler, we're done + if (!normalizedCalls.length || !runTools) { + break; + } + + // Execute tools and continue the loop + const results = await runTools(normalizedCalls); + + // Add assistant message with tool calls + messages.push({ + role: "assistant", + content: textParts.join("") || null, + tool_calls: normalizedCalls.map((c) => ({ + id: c.id, + type: "function" as const, + function: { + name: c.name, + arguments: JSON.stringify(c.input), + }, + })), + }); + + // Add tool results + for (const r of results) { + messages.push({ + role: "tool", + tool_call_id: r.tool_use_id, + content: r.content, + }); + } + } + + return { fullText }; +} + +export async function completeOpenRouterText(params: { + model: string; + systemPrompt?: string; + user: string; + maxTokens?: number; + apiKeys?: { openrouter?: string | null }; +}): Promise { + const apiKey = getApiKey(params.apiKeys?.openrouter); + const openRouterModel = toOpenRouterModelId(params.model); + + const messages: OpenRouterMessage[] = []; + if (params.systemPrompt) { + messages.push({ role: "system", content: params.systemPrompt }); + } + messages.push({ role: "user", content: params.user }); + + const response = await fetch(OPENROUTER_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "HTTP-Referer": process.env.APP_URL || "http://localhost:3000", + "X-Title": "Mike", + }, + body: JSON.stringify({ + model: openRouterModel, + messages, + max_tokens: params.maxTokens ?? 512, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenRouter API error: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + return data.choices?.[0]?.message?.content ?? ""; +} diff --git a/backend/src/lib/llm/types.ts b/backend/src/lib/llm/types.ts index 8cc411a7..4858972b 100644 --- a/backend/src/lib/llm/types.ts +++ b/backend/src/lib/llm/types.ts @@ -2,7 +2,7 @@ // Callers always speak OpenAI-style tools + { role, content } messages; each // provider translates internally. -export type Provider = "claude" | "gemini"; +export type Provider = "claude" | "gemini" | "openrouter"; export type OpenAIToolSchema = { type: "function"; @@ -39,6 +39,7 @@ export type StreamCallbacks = { export type UserApiKeys = { claude?: string | null; gemini?: string | null; + openrouter?: string | null; }; export type StreamChatParams = { diff --git a/backend/src/lib/mcp/client.ts b/backend/src/lib/mcp/client.ts new file mode 100644 index 00000000..382d5258 --- /dev/null +++ b/backend/src/lib/mcp/client.ts @@ -0,0 +1,161 @@ +// Thin wrapper around the MCP TypeScript SDK's Streamable-HTTP client. +// +// Mike opens one client per (user, MCP server) per chat request. Connections +// are short-lived: we initialize, list tools, run any tools the model calls, +// then close in a `finally` on the request handler. There is no connection +// pool — each chat request pays an `initialize` round-trip per enabled +// server. This keeps the design stateless and avoids needing a worker. + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; +import type { Tool } from "@modelcontextprotocol/sdk/types.js"; + +const CONNECT_TIMEOUT_MS = 10_000; +const CALL_TIMEOUT_MS = 60_000; + +export class McpHttpClient { + private client: Client | null = null; + private transport: StreamableHTTPClientTransport | null = null; + + constructor( + private readonly url: string, + private readonly headers: Record, + private readonly authProvider?: OAuthClientProvider, + ) {} + + async connect(): Promise { + this.transport = new StreamableHTTPClientTransport(new URL(this.url), { + requestInit: { + headers: this.headers, + }, + ...(this.authProvider ? { authProvider: this.authProvider } : {}), + }); + this.client = new Client( + { name: "mike", version: "1.0.0" }, + { capabilities: {} }, + ); + await withTimeout( + this.client.connect(this.transport), + CONNECT_TIMEOUT_MS, + "MCP connect", + ); + } + + async listTools(): Promise { + if (!this.client) throw new Error("MCP client not connected"); + const result = await withTimeout( + this.client.listTools(), + CONNECT_TIMEOUT_MS, + "MCP listTools", + ); + return result.tools as Tool[]; + } + + /** + * Calls a tool and returns a structured {ok, content, truncated} result. + * Errors (transport failures, MCP `isError`) become ok=false with the + * error message in `content` so the model can surface them rather than + * crashing the chat. `content` is hard-capped at MAX_TOOL_CONTENT_BYTES + * (configurable via MCP_MAX_TOOL_BYTES) to prevent a misbehaving connector + * from blowing the LLM context window or DoSing the chat. + */ + async callTool( + name: string, + args: Record, + ): Promise<{ ok: boolean; content: string; truncated: boolean }> { + if (!this.client) { + return { + ok: false, + content: "MCP client not connected", + truncated: false, + }; + } + try { + const result = await withTimeout( + this.client.callTool({ name, arguments: args }), + CALL_TIMEOUT_MS, + `MCP callTool(${name})`, + ); + const blocks = (result.content ?? []) as Array<{ + type?: string; + text?: string; + }>; + const text = blocks + .filter((b) => b?.type === "text" && typeof b.text === "string") + .map((b) => b.text) + .join("\n\n"); + if (result.isError) { + return capContent( + false, + `Error: ${text || "(no detail)"}`, + ); + } + return capContent(true, text || "(tool returned no text content)"); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { ok: false, content: `Failed: ${msg}`, truncated: false }; + } + } + + async close(): Promise { + try { + await this.client?.close(); + } catch { + /* ignore */ + } + try { + await this.transport?.close(); + } catch { + /* ignore */ + } + this.client = null; + this.transport = null; + } +} + +// Cap tool output before it reaches the LLM. A misbehaving connector that +// returns multi-megabyte responses would blow the model's context window, +// rack up token cost, and effectively DoS the chat. Default 64 KB; override +// via MCP_MAX_TOOL_BYTES. +const MAX_TOOL_CONTENT_BYTES = (() => { + const raw = Number(process.env.MCP_MAX_TOOL_BYTES); + return Number.isFinite(raw) && raw > 0 ? raw : 64 * 1024; +})(); + +function capContent( + ok: boolean, + raw: string, +): { ok: boolean; content: string; truncated: boolean } { + const buf = Buffer.from(raw, "utf8"); + if (buf.byteLength <= MAX_TOOL_CONTENT_BYTES) { + return { ok, content: raw, truncated: false }; + } + const head = buf.subarray(0, MAX_TOOL_CONTENT_BYTES).toString("utf8"); + const skipped = buf.byteLength - MAX_TOOL_CONTENT_BYTES; + const marker = `\n\n[…truncated ${skipped} bytes; raise MCP_MAX_TOOL_BYTES to see more]`; + return { ok, content: head + marker, truncated: true }; +} + +function withTimeout( + p: Promise, + ms: number, + label: string, +): Promise { + return new Promise((resolve, reject) => { + const t = setTimeout( + () => reject(new Error(`${label} timed out after ${ms}ms`)), + ms, + ); + p.then( + (v) => { + clearTimeout(t); + resolve(v); + }, + (e) => { + clearTimeout(t); + reject(e); + }, + ); + }); +} diff --git a/backend/src/lib/mcp/oauth.ts b/backend/src/lib/mcp/oauth.ts new file mode 100644 index 00000000..4177ca81 --- /dev/null +++ b/backend/src/lib/mcp/oauth.ts @@ -0,0 +1,282 @@ +// OAuth 2.1 client glue for MCP connectors. +// +// The MCP SDK does almost all of the heavy lifting via its `auth()` helper — +// RFC 9728 discovery, dynamic client registration (RFC 7591), PKCE (S256), +// authorization-code exchange, and token refresh. We only have to plug in an +// `OAuthClientProvider` whose getters/setters read and write the row's +// oauth_* columns, plus a thin HMAC-signed state token so the callback can +// look the row up without a server-side session. + +import crypto from "crypto"; +import type { + OAuthClientInformationFull, + OAuthClientInformationMixed, + OAuthClientMetadata, + OAuthTokens, +} from "@modelcontextprotocol/sdk/shared/auth.js"; +import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"; +import { + decryptApiKey, + decryptJsonBlob, + encryptApiKey, + encryptJsonBlob, +} from "../apiKeys"; +import { getSigningSecret } from "../downloadTokens"; +import type { createServerSupabase } from "../supabase"; + +const STATE_TTL_SECONDS = 5 * 60; // 5 minutes +const CLIENT_NAME = "Mike"; +const CLIENT_URI = "https://github.com/willchen96/mike"; + +function backendPublicUrl(): string { + const url = process.env.BACKEND_PUBLIC_URL?.trim(); + if (url) return url.replace(/\/+$/, ""); + const port = process.env.PORT ?? "3001"; + return `http://localhost:${port}`; +} + +export function oauthCallbackUrl(): string { + return `${backendPublicUrl()}/mcp/oauth/callback`; +} + +// --------------------------------------------------------------------------- +// State token (CSRF + flow continuation across the popup hop). +// HMAC-signed, no DB round-trip; encodes { user_id, server_id, exp }. +// Reuses DOWNLOAD_SIGNING_SECRET — the same secret already gates download +// tokens and would already have to be rotated on compromise. +// --------------------------------------------------------------------------- + +export function signOAuthState(payload: { + user_id: string; + server_id: string; +}): string { + const body = { + ...payload, + exp: Math.floor(Date.now() / 1000) + STATE_TTL_SECONDS, + }; + const enc = Buffer.from(JSON.stringify(body), "utf8").toString("base64url"); + const sig = crypto.createHmac("sha256", getSigningSecret()).update(enc).digest(); + return `${enc}.${sig.toString("base64url")}`; +} + +export function verifyOAuthState( + token: string, +): { user_id: string; server_id: string } | null { + const parts = token.split("."); + if (parts.length !== 2) return null; + const [enc, sigEnc] = parts; + const expected = crypto + .createHmac("sha256", getSigningSecret()) + .update(enc) + .digest(); + const expectedEnc = expected.toString("base64url"); + if (sigEnc.length !== expectedEnc.length) return null; + if ( + !crypto.timingSafeEqual(Buffer.from(sigEnc), Buffer.from(expectedEnc)) + ) { + return null; + } + try { + const body = JSON.parse(Buffer.from(enc, "base64url").toString("utf8")) as { + user_id: string; + server_id: string; + exp: number; + }; + if (!body.user_id || !body.server_id) return null; + if (Math.floor(Date.now() / 1000) > body.exp) return null; + return { user_id: body.user_id, server_id: body.server_id }; + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// OAuthClientProvider implementation backed by user_mcp_servers row. +// +// Two modes: +// - "initiate": from POST /oauth/start. The SDK's auth() will discover, DCR +// if needed, generate PKCE, and call redirectToAuthorization() with the +// authorize URL. We capture that URL into `lastAuthorizeUrl` and the +// route returns it to the frontend popup. +// - "use": from a chat request. If the SDK needs a fresh authorization +// (refresh failed or never happened), redirectToAuthorization() throws +// so the caller can mark the row reauth_required and surface to the UI. +// --------------------------------------------------------------------------- + +export type OAuthProviderMode = "initiate" | "use"; + +export class DbOAuthProvider implements OAuthClientProvider { + private metadataCache: Record | null = null; + private tokensCache: OAuthTokens | null = null; + private codeVerifierCache: string | null = null; + private mode: OAuthProviderMode; + private signedState: string; + + /** Set by redirectToAuthorization() in `initiate` mode. */ + public lastAuthorizeUrl: URL | null = null; + + constructor( + private readonly db: ReturnType, + private readonly serverId: string, + private readonly userId: string, + mode: OAuthProviderMode, + ) { + this.mode = mode; + this.signedState = signOAuthState({ + user_id: userId, + server_id: serverId, + }); + } + + get redirectUrl(): string { + return oauthCallbackUrl(); + } + + get clientMetadata(): OAuthClientMetadata { + return { + client_name: CLIENT_NAME, + client_uri: CLIENT_URI, + redirect_uris: [oauthCallbackUrl()], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + // Public client — no client secret stored, PKCE-protected. + token_endpoint_auth_method: "none", + }; + } + + state(): string { + return this.signedState; + } + + async clientInformation(): Promise< + OAuthClientInformationMixed | undefined + > { + await this.loadMetadata(); + const ci = (this.metadataCache as { client?: OAuthClientInformationFull } | null) + ?.client; + return ci ?? undefined; + } + + async saveClientInformation( + info: OAuthClientInformationMixed, + ): Promise { + await this.loadMetadata(); + const next = { ...(this.metadataCache ?? {}), client: info }; + this.metadataCache = next; + await this.db + .from("user_mcp_servers") + .update({ oauth_metadata: next }) + .eq("id", this.serverId); + } + + async tokens(): Promise { + if (this.tokensCache) return this.tokensCache; + const { data } = await this.db + .from("user_mcp_servers") + .select("oauth_tokens") + .eq("id", this.serverId) + .single(); + // oauth_tokens may be either an `enc:v1:` ciphertext string or, for + // rows written before encryption-at-rest landed, the raw token jsonb. + // decryptJsonBlob returns plaintext in both cases. + const t = decryptJsonBlob(data?.oauth_tokens); + this.tokensCache = t; + return t ?? undefined; + } + + async saveTokens(tokens: OAuthTokens): Promise { + this.tokensCache = tokens; + // The PKCE verifier is one-shot; no point persisting after token + // exchange. Clearing also keeps the row tidy on subsequent refreshes. + this.codeVerifierCache = null; + await this.db + .from("user_mcp_servers") + .update({ + oauth_tokens: encryptJsonBlob(tokens), + oauth_code_verifier: null, + last_error: null, + }) + .eq("id", this.serverId); + } + + async redirectToAuthorization(authorizationUrl: URL): Promise { + if (this.mode === "initiate") { + this.lastAuthorizeUrl = authorizationUrl; + return; + } + // In "use" mode (mid-chat), we have nowhere to redirect the user; the + // caller will catch this and mark the row reauth_required so the UI + // prompts the user to re-sign in from settings. + throw new ReauthRequiredError( + `Connector requires re-sign-in (would redirect to ${authorizationUrl.origin})`, + ); + } + + async saveCodeVerifier(codeVerifier: string): Promise { + this.codeVerifierCache = codeVerifier; + await this.db + .from("user_mcp_servers") + .update({ oauth_code_verifier: encryptApiKey(codeVerifier) }) + .eq("id", this.serverId); + } + + async codeVerifier(): Promise { + if (this.codeVerifierCache) return this.codeVerifierCache; + const { data } = await this.db + .from("user_mcp_servers") + .select("oauth_code_verifier") + .eq("id", this.serverId) + .single(); + if (!data?.oauth_code_verifier) { + throw new Error("Missing PKCE verifier — start the flow again"); + } + // decryptApiKey passes legacy plaintext through unchanged, so a row + // written before encryption-at-rest landed still works mid-flow. + const plaintext = decryptApiKey(data.oauth_code_verifier); + if (!plaintext) { + throw new Error("Missing PKCE verifier — start the flow again"); + } + this.codeVerifierCache = plaintext; + return plaintext; + } + + async invalidateCredentials( + scope: "all" | "client" | "tokens" | "verifier" | "discovery", + ): Promise { + const update: Record = {}; + if (scope === "all" || scope === "tokens") + update.oauth_tokens = null; + if (scope === "all" || scope === "client" || scope === "discovery") + update.oauth_metadata = null; + if (scope === "all" || scope === "verifier") + update.oauth_code_verifier = null; + if (Object.keys(update).length === 0) return; + await this.db + .from("user_mcp_servers") + .update(update) + .eq("id", this.serverId); + if (update.oauth_tokens === null) this.tokensCache = null; + if (update.oauth_metadata === null) this.metadataCache = null; + if (update.oauth_code_verifier === null) this.codeVerifierCache = null; + } + + private async loadMetadata(): Promise { + if (this.metadataCache !== null) return; + const { data } = await this.db + .from("user_mcp_servers") + .select("oauth_metadata") + .eq("id", this.serverId) + .single(); + this.metadataCache = (data?.oauth_metadata ?? {}) as Record< + string, + unknown + >; + } +} + +export class ReauthRequiredError extends Error { + constructor(message?: string) { + super(message ?? "Re-authorization required"); + this.name = "ReauthRequiredError"; + } +} diff --git a/backend/src/lib/mcp/servers.ts b/backend/src/lib/mcp/servers.ts new file mode 100644 index 00000000..3c47a88b --- /dev/null +++ b/backend/src/lib/mcp/servers.ts @@ -0,0 +1,219 @@ +// Per-request MCP server loader. +// +// Called once at the top of each chat request. Reads the user's enabled MCP +// servers from Postgres, opens a Streamable-HTTP client to each in parallel, +// fetches its tool list, and converts each tool to the OpenAI-style schema +// Mike's LLM adapter speaks. Tool names are prefixed with `mcp____` so +// the dispatcher in chatTools can route calls back to the right server. + +import { createHash } from "crypto"; +import { + decryptJsonBlob, + encryptJsonBlob, + needsJsonBlobUpgrade, +} from "../apiKeys"; +import type { OpenAIToolSchema } from "../llm/types"; +import type { createServerSupabase } from "../supabase"; +import { McpHttpClient } from "./client"; +import { DbOAuthProvider, ReauthRequiredError } from "./oauth"; +import type { LoadedMcpServer, McpServerRow } from "./types"; + +/** + * Decodes the credential-bearing jsonb columns on a freshly-fetched row, + * mutating the row in place to expose plaintext to the rest of the loader. + * Returns a partial UPDATE patch for any column that was stored as legacy + * plaintext and should be re-written encrypted; the caller fires that off + * best-effort so the hot path isn't blocked on the upgrade write. + * + * `oauth_code_verifier` is a per-text-column secret read on demand by + * DbOAuthProvider.codeVerifier(); we don't touch it here. + */ +export function decryptMcpRowCredentials( + row: McpServerRow, +): Partial> { + const upgrades: Partial> = {}; + + const rawHeaders = row.headers as unknown; + const decryptedHeaders = + decryptJsonBlob>(rawHeaders) ?? {}; + row.headers = decryptedHeaders; + if (needsJsonBlobUpgrade(rawHeaders)) { + const enc = encryptJsonBlob(decryptedHeaders); + if (enc) upgrades.headers = enc; + } + + const rawTokens = row.oauth_tokens as unknown; + const decryptedTokens = decryptJsonBlob>(rawTokens); + row.oauth_tokens = decryptedTokens; + if (needsJsonBlobUpgrade(rawTokens) && decryptedTokens) { + const enc = encryptJsonBlob(decryptedTokens); + if (enc) upgrades.oauth_tokens = enc; + } + + return upgrades; +} + +const TOOL_NAME_MAX = 64; +const TOOL_PREFIX = "mcp__"; + +export async function loadEnabledMcpServersForUser( + userId: string, + db: ReturnType, +): Promise { + const { data, error } = await db + .from("user_mcp_servers") + .select("*") + .eq("user_id", userId) + .eq("enabled", true); + if (error || !data || data.length === 0) return []; + + const rows = data as McpServerRow[]; + + // Decrypt credential columns in place + opportunistically rewrite any + // legacy plaintext rows in the background. We don't await the upgrade + // so a failing-forever encryption write can't block tool loading on + // every chat turn — but we do log it so callers can detect it. + for (const row of rows) { + const upgrades = decryptMcpRowCredentials(row); + if (Object.keys(upgrades).length > 0) { + void db + .from("user_mcp_servers") + .update(upgrades) + .eq("id", row.id) + .then(({ error: upgErr }) => { + if (upgErr) { + console.warn( + `[mcp] failed to encrypt-at-rest upgrade row ${row.id}: ${upgErr.message}`, + ); + } + }); + } + } + + const results = await Promise.allSettled( + rows.map((row) => loadOne(row, userId, db)), + ); + + const out: LoadedMcpServer[] = []; + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const row = rows[i]; + if (r.status === "fulfilled" && r.value) { + out.push(r.value); + // Clear stale error on success. + if (row.last_error) { + await db + .from("user_mcp_servers") + .update({ last_error: null }) + .eq("id", row.id); + } + } else { + const reason = r.status === "rejected" ? r.reason : "unknown error"; + const isReauth = reason instanceof ReauthRequiredError; + const err = + reason instanceof Error ? reason.message : String(reason); + console.warn( + `[mcp] failed to load server ${row.slug} (${row.url}): ${err}`, + ); + await db + .from("user_mcp_servers") + .update({ + last_error: isReauth + ? "reauth_required" + : err.slice(0, 1000), + }) + .eq("id", row.id); + } + } + return out; +} + +async function loadOne( + row: McpServerRow, + userId: string, + db: ReturnType, +): Promise { + let authProvider: DbOAuthProvider | undefined; + if (row.auth_type === "oauth") { + // No tokens yet → don't even try to connect; the UI will surface a + // "Sign in" affordance and the user kicks off /oauth/start. + if (!row.oauth_tokens) { + throw new ReauthRequiredError( + "Connector not yet authorized — sign in from settings", + ); + } + authProvider = new DbOAuthProvider(db, row.id, userId, "use"); + } + const client = new McpHttpClient( + row.url, + row.headers ?? {}, + authProvider, + ); + await client.connect(); + const mcpTools = await client.listTools(); + + const tools: OpenAIToolSchema[] = []; + const toolNameMap = new Map(); + for (const t of mcpTools) { + const prefixed = prefixedToolName(row.slug, t.name); + toolNameMap.set(prefixed, t.name); + tools.push({ + type: "function", + function: { + name: prefixed, + description: `[${row.name}] ${t.description ?? ""}`.trim(), + parameters: (t.inputSchema as Record) ?? { + type: "object", + properties: {}, + }, + }, + }); + } + + return { + row, + tools, + toolNameMap, + client: { + callTool: (name, args) => client.callTool(name, args), + close: () => client.close(), + }, + }; +} + +/** + * `mcp____`, capped at 64 chars (Anthropic's limit). + * If the natural name is too long, the toolName tail is replaced with a + * 12-hex-char hash so the prefix stays intact and the dispatcher can route. + */ +export function prefixedToolName(slug: string, toolName: string): string { + const natural = `${TOOL_PREFIX}${slug}__${toolName}`; + if (natural.length <= TOOL_NAME_MAX) return natural; + const hash = createHash("sha256") + .update(toolName) + .digest("hex") + .slice(0, 12); + const head = `${TOOL_PREFIX}${slug}__`; + const room = TOOL_NAME_MAX - head.length - 1 /* underscore */ - hash.length; + const truncated = toolName.slice(0, Math.max(0, room)); + return `${head}${truncated}_${hash}`.slice(0, TOOL_NAME_MAX); +} + +export async function closeMcpServers(servers: LoadedMcpServer[]): Promise { + await Promise.allSettled(servers.map((s) => s.client.close())); +} + +/** + * Look up which loaded server owns a prefixed tool name. Used by the chat + * tool dispatcher. + */ +export function findMcpServerForTool( + prefixedName: string, + servers: LoadedMcpServer[], +): { server: LoadedMcpServer; originalName: string } | null { + for (const s of servers) { + const original = s.toolNameMap.get(prefixedName); + if (original) return { server: s, originalName: original }; + } + return null; +} diff --git a/backend/src/lib/mcp/types.ts b/backend/src/lib/mcp/types.ts new file mode 100644 index 00000000..84d60668 --- /dev/null +++ b/backend/src/lib/mcp/types.ts @@ -0,0 +1,49 @@ +import type { OpenAIToolSchema } from "../llm/types"; + +export type McpServerRow = { + id: string; + user_id: string; + slug: string; + name: string; + url: string; + headers: Record; + enabled: boolean; + last_error: string | null; + auth_type: "headers" | "oauth"; + oauth_metadata: Record | null; + oauth_tokens: Record | null; + oauth_code_verifier: string | null; +}; + +/** + * One MCP server, opened for the duration of a single chat request. + * + * `tools` are already prefixed (`mcp____`) and ready to merge + * into the per-request tool list. The original tool name is preserved in + * `toolNameMap` so the dispatcher can call back into the MCP server with the + * unprefixed name. + */ +export type LoadedMcpServer = { + row: McpServerRow; + tools: OpenAIToolSchema[]; + /** prefixed tool name → original MCP tool name */ + toolNameMap: Map; + client: { + callTool: ( + toolName: string, + args: Record, + ) => Promise; + close: () => Promise; + }; +}; + +/** + * Structured result from an MCP tool call. `ok` carries success vs error + * directly so callers don't have to sniff message prefixes; `truncated` is + * true when `content` was clipped to the model-side size cap. + */ +export type McpToolResult = { + ok: boolean; + content: string; + truncated: boolean; +}; diff --git a/backend/src/lib/upload.ts b/backend/src/lib/upload.ts index caa44dbf..5dbc646e 100644 --- a/backend/src/lib/upload.ts +++ b/backend/src/lib/upload.ts @@ -1,10 +1,12 @@ import type { RequestHandler } from "express"; +import JSZip from "jszip"; import multer from "multer"; export const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024; export const MAX_UPLOAD_SIZE_MB = Math.round( MAX_UPLOAD_SIZE_BYTES / (1024 * 1024), ); +const MAX_DOCX_UNCOMPRESSED_BYTES = 100 * 1024 * 1024; const memoryUpload = multer({ storage: multer.memoryStorage(), @@ -34,3 +36,85 @@ export function singleFileUpload(fieldName: string): RequestHandler { }); }; } + +export const ALLOWED_DOCUMENT_TYPES = new Set(["pdf", "docx", "doc"]); + +export type ValidatedDocumentUpload = { + suffix: "pdf" | "docx" | "doc"; + contentType: string; +}; + +export async function validateDocumentUpload( + file: Express.Multer.File, +): Promise { + const suffix = getFileSuffix(file.originalname); + if (!suffix || !ALLOWED_DOCUMENT_TYPES.has(suffix)) { + throw new Error( + `Unsupported file type: ${suffix}. Allowed: pdf, docx, doc`, + ); + } + + if (suffix === "pdf") { + // PDF 1.4 §3.4.1 allows %PDF- to appear within the first 1024 bytes, + // not necessarily at offset 0 (e.g. leading whitespace or BOM). + const head = file.buffer.subarray(0, Math.min(1024, file.buffer.length)); + if (!head.includes(Buffer.from("%PDF-"))) { + throw new Error("Uploaded PDF does not have a valid PDF header."); + } + return { suffix, contentType: "application/pdf" }; + } + + if (suffix === "doc") { + const oleMagic = Buffer.from([ + 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, + ]); + if (!file.buffer.subarray(0, 8).equals(oleMagic)) { + throw new Error("Uploaded DOC does not have a valid legacy Word header."); + } + return { + suffix, + contentType: "application/msword", + }; + } + + try { + const zip = await JSZip.loadAsync(file.buffer); + if (!zip.file("[Content_Types].xml") || !zip.file("word/document.xml")) { + throw new Error("Uploaded DOCX is missing required Word document parts."); + } + + // Decompressing each entry to measure size is necessary because the + // private JSZip._data.uncompressedSize field returns undefined for + // store-only (method=0) entries, allowing a zip bomb to bypass the check. + let totalUncompressed = 0; + for (const [, entry] of Object.entries(zip.files)) { + if (entry.dir) continue; + const buf = await entry.async("nodebuffer"); + totalUncompressed += buf.byteLength; + if (totalUncompressed > MAX_DOCX_UNCOMPRESSED_BYTES) { + throw new Error("Uploaded DOCX expands beyond the allowed size."); + } + } + } catch (err) { + if (err instanceof Error && err.message.startsWith("Uploaded DOCX")) { + throw err; + } + throw new Error("Uploaded DOCX is not a valid Word archive."); + } + + return { + suffix, + contentType: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }; +} + +function getFileSuffix(filename: string): ValidatedDocumentUpload["suffix"] | null { + const suffix = filename.includes(".") + ? filename.split(".").pop()!.toLowerCase() + : ""; + if (suffix === "pdf" || suffix === "docx" || suffix === "doc") { + return suffix; + } + return null; +} diff --git a/backend/src/lib/userSettings.ts b/backend/src/lib/userSettings.ts index c798b636..ce07897c 100644 --- a/backend/src/lib/userSettings.ts +++ b/backend/src/lib/userSettings.ts @@ -1,4 +1,8 @@ import { createServerSupabase } from "./supabase"; +import { + decryptApiKey, + buildPlaintextUpgrades, +} from "./apiKeys"; import { resolveModel, DEFAULT_TITLE_MODEL, @@ -29,14 +33,11 @@ export async function getUserModelSettings( const client = db ?? createServerSupabase(); const { data } = await client .from("user_profiles") - .select("tabular_model, claude_api_key, gemini_api_key") + .select("tabular_model, claude_api_key, gemini_api_key, openrouter_api_key") .eq("user_id", userId) .single(); - const api_keys: UserApiKeys = { - claude: data?.claude_api_key ?? null, - gemini: data?.gemini_api_key ?? null, - }; + const api_keys = await decryptAndUpgradeApiKeys(userId, data, client); return { title_model: resolveTitleModel(api_keys), @@ -52,11 +53,43 @@ export async function getUserApiKeys( const client = db ?? createServerSupabase(); const { data } = await client .from("user_profiles") - .select("claude_api_key, gemini_api_key") + .select("claude_api_key, gemini_api_key, openrouter_api_key") .eq("user_id", userId) .single(); - return { - claude: data?.claude_api_key ?? null, - gemini: data?.gemini_api_key ?? null, + return decryptAndUpgradeApiKeys(userId, data, client); +} + +async function decryptAndUpgradeApiKeys( + userId: string, + data: + | { + claude_api_key?: string | null; + gemini_api_key?: string | null; + openrouter_api_key?: string | null; + } + | null, + client: ReturnType, +): Promise { + const storedClaude = data?.claude_api_key ?? null; + const storedGemini = data?.gemini_api_key ?? null; + const storedOpenrouter = data?.openrouter_api_key ?? null; + const apiKeys: UserApiKeys = { + claude: decryptApiKey(storedClaude), + gemini: decryptApiKey(storedGemini), + openrouter: decryptApiKey(storedOpenrouter), }; + + const updates = buildPlaintextUpgrades({ + claude_api_key: storedClaude, + gemini_api_key: storedGemini, + openrouter_api_key: storedOpenrouter, + }); + if (Object.keys(updates).length > 0) { + await client + .from("user_profiles") + .update({ ...updates, updated_at: new Date().toISOString() }) + .eq("user_id", userId); + } + + return apiKeys; } diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index b56c2936..6afcd54a 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -12,7 +12,11 @@ import { } from "../lib/chatTools"; import { completeText } from "../lib/llm"; import { getUserApiKeys, getUserModelSettings } from "../lib/userSettings"; -import { checkProjectAccess } from "../lib/access"; +import { checkProjectAccess, listAccessibleProjectIds } from "../lib/access"; +import { + closeMcpServers, + loadEnabledMcpServersForUser, +} from "../lib/mcp/servers"; export const chatRouter = Router(); @@ -24,6 +28,7 @@ export const chatRouter = Router(); // listed per-project via GET /projects/:projectId/chats. chatRouter.get("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; + const userEmail = res.locals.userEmail as string | undefined; const db = createServerSupabase(); const { data: ownProjects, error: projErr } = await db @@ -46,14 +51,29 @@ chatRouter.get("/", requireAuth, async (req, res) => { .or(filter) .order("created_at", { ascending: false }); if (error) return void res.status(500).json({ detail: error.message }); - res.json(data ?? []); + const accessibleProjectIds = new Set( + await listAccessibleProjectIds(userId, userEmail, db), + ); + res.json( + (data ?? []).filter((chat) => { + const projectId = chat.project_id as string | null; + if (!projectId) return chat.user_id === userId; + return accessibleProjectIds.has(projectId); + }), + ); }); // POST /chat/create chatRouter.post("/create", requireAuth, async (req, res) => { const userId = res.locals.userId as string; + const userEmail = res.locals.userEmail as string | undefined; const projectId: string | null = req.body.project_id ?? null; const db = createServerSupabase(); + if (projectId) { + const access = await checkProjectAccess(projectId, userId, userEmail, db); + if (!access.ok) + return void res.status(404).json({ detail: "Project not found" }); + } const { data, error } = await db .from("chats") .insert({ user_id: userId, project_id: projectId ?? undefined }) @@ -78,9 +98,10 @@ chatRouter.get("/:chatId", requireAuth, async (req, res) => { .single(); if (error || !chat) return void res.status(404).json({ detail: "Chat not found" }); - // Owner of the chat OR a member of the chat's project can view it. - let canView = chat.user_id === userId; - if (!canView && chat.project_id) { + // Standalone chats stay owner-only. Project chats require current project + // access so revoked shares cannot keep using old chat IDs. + let canView = false; + if (chat.project_id) { const access = await checkProjectAccess( chat.project_id, userId, @@ -88,6 +109,8 @@ chatRouter.get("/:chatId", requireAuth, async (req, res) => { db, ); canView = access.ok; + } else { + canView = chat.user_id === userId; } if (!canView) return void res.status(404).json({ detail: "Chat not found" }); @@ -274,8 +297,8 @@ chatRouter.post("/:chatId/generate-title", requireAuth, async (req, res) => { if (error || !chat) return void res.status(404).json({ detail: "Chat not found" }); - let canTitle = chat.user_id === userId; - if (!canTitle && chat.project_id) { + let canTitle = false; + if (chat.project_id) { const access = await checkProjectAccess( chat.project_id, userId, @@ -283,6 +306,8 @@ chatRouter.post("/:chatId/generate-title", requireAuth, async (req, res) => { db, ); canTitle = access.ok; + } else { + canTitle = chat.user_id === userId; } if (!canTitle) return void res.status(404).json({ detail: "Chat not found" }); @@ -323,28 +348,24 @@ chatRouter.post("/", requireAuth, async (req, res) => { model?: string; }; - console.log("[chat/stream] incoming request", { - userId, - chat_id, - project_id, - model, - messageCount: messages?.length, - }); - const userEmail = res.locals.userEmail as string | undefined; const db = createServerSupabase(); let chatId = chat_id ?? null; let chatTitle: string | null = null; + let effectiveProjectId: string | null = project_id ?? null; if (chatId) { - // Either chat owner OR a member of the chat's project can post. + // Standalone chats stay owner-only. Project chats require current + // project access, using the project_id stored on the chat row. const { data: existing } = await db .from("chats") .select("id, title, user_id, project_id") .eq("id", chatId) .single(); - let canUse = !!existing && existing.user_id === userId; - if (!canUse && existing?.project_id) { + if (!existing) + return void res.status(404).json({ detail: "Chat not found" }); + let canUse = false; + if (existing.project_id) { const access = await checkProjectAccess( existing.project_id, userId, @@ -352,9 +373,13 @@ chatRouter.post("/", requireAuth, async (req, res) => { db, ); canUse = access.ok; + } else { + canUse = existing.user_id === userId; } - if (!canUse || !existing) chatId = null; - else chatTitle = existing.title; + if (!canUse) + return void res.status(404).json({ detail: "Chat not found" }); + chatTitle = existing.title; + effectiveProjectId = (existing.project_id as string | null) ?? null; } if (!chatId) { @@ -372,9 +397,10 @@ chatRouter.post("/", requireAuth, async (req, res) => { .status(404) .json({ detail: "Project not found" }); } + effectiveProjectId = project_id ?? null; const { data: newChat, error } = await db .from("chats") - .insert({ user_id: userId, project_id: project_id ?? null }) + .insert({ user_id: userId, project_id: effectiveProjectId }) .select("id, title") .single(); if (error || !newChat) { @@ -387,8 +413,6 @@ chatRouter.post("/", requireAuth, async (req, res) => { chatTitle = newChat.title; } - console.log("[chat/stream] resolved chatId", chatId); - const lastUser = [...messages].reverse().find((m) => m.role === "user"); if (lastUser) { await db.from("chat_messages").insert({ @@ -420,12 +444,6 @@ chatRouter.post("/", requireAuth, async (req, res) => { const workflowStore = await buildWorkflowStore(userId, userEmail, db); - console.log("[chat/stream] starting LLM stream", { - apiMessageCount: apiMessages.length, - docCount: Object.keys(docIndex).length, - workflowCount: Object.keys(workflowStore).length, - }); - res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); @@ -435,6 +453,7 @@ chatRouter.post("/", requireAuth, async (req, res) => { const write = (line: string) => res.write(line); const apiKeys = await getUserApiKeys(userId, db); + const mcpServers = await loadEnabledMcpServersForUser(userId, db); try { write(`data: ${JSON.stringify({ type: "chat_id", chatId })}\n\n`); @@ -449,12 +468,8 @@ chatRouter.post("/", requireAuth, async (req, res) => { workflowStore, model, apiKeys, - projectId: project_id ?? null, - }); - - console.log("[chat/stream] LLM stream finished", { - fullTextLen: fullText?.length ?? 0, - eventCount: events?.length ?? 0, + projectId: effectiveProjectId, + mcpServers, }); const annotations = extractAnnotations(fullText, docIndex, events); @@ -482,6 +497,7 @@ chatRouter.post("/", requireAuth, async (req, res) => { /* ignore */ } } finally { + await closeMcpServers(mcpServers); res.end(); } }); diff --git a/backend/src/routes/documents.ts b/backend/src/routes/documents.ts index 32f4b881..f9b1e2b9 100644 --- a/backend/src/routes/documents.ts +++ b/backend/src/routes/documents.ts @@ -22,10 +22,9 @@ import { loadActiveVersion, } from "../lib/documentVersions"; import { ensureDocAccess } from "../lib/access"; -import { singleFileUpload } from "../lib/upload"; +import { singleFileUpload, validateDocumentUpload } from "../lib/upload"; export const documentsRouter = Router(); -const ALLOWED_TYPES = new Set(["pdf", "docx", "doc"]); // GET /single-documents documentsRouter.get("/", requireAuth, async (req, res) => { @@ -401,11 +400,19 @@ documentsRouter.post( if (!access.ok) return void res.status(404).json({ detail: "Document not found" }); - // Reject if the uploaded file's extension doesn't match the document's - // declared type — otherwise every downstream viewer/extractor breaks. - const suffix = file.originalname.includes(".") - ? file.originalname.split(".").pop()!.toLowerCase() - : ""; + let validated: Awaited>; + try { + validated = await validateDocumentUpload(file); + } catch (err) { + return void res.status(400).json({ + detail: err instanceof Error ? err.message : "Invalid upload", + }); + } + const suffix = validated.suffix; + + // Reject if the uploaded file's extension/content doesn't match the + // document's declared type — otherwise every downstream viewer/extractor + // breaks or processes an unexpected format. if (doc.file_type && suffix && doc.file_type !== suffix) { return void res.status(400).json({ detail: `Uploaded file type (${suffix}) does not match document type (${doc.file_type}).`, @@ -421,10 +428,6 @@ documentsRouter.post( versionSlug, file.originalname, ); - const contentType = - suffix === "pdf" - ? "application/pdf" - : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; try { await uploadFile( key, @@ -432,7 +435,7 @@ documentsRouter.post( file.buffer.byteOffset, file.buffer.byteOffset + file.buffer.byteLength, ) as ArrayBuffer, - contentType, + validated.contentType, ); } catch (e) { console.error("[versions/upload] storage write failed", e); @@ -459,10 +462,7 @@ documentsRouter.post( ); pdfStoragePath = pdfKey; } catch (err) { - console.error( - `[versions/upload] DOCX→PDF conversion failed for ${file.originalname}:`, - err, - ); + console.error("[versions/upload] DOCX→PDF conversion failed", err); } } else if (suffix === "pdf") { // For PDF uploads, the uploaded bytes are themselves the PDF rendition. @@ -632,43 +632,31 @@ async function handleEditResolution( const { documentId, editId } = req.params; const db = createServerSupabase(); - console.log(`[edit-resolution] incoming ${mode}`, { - userId, - documentId, - editId, - }); - const { data: edit, error: editErr } = await db .from("document_edits") .select("id, document_id, change_id, del_w_id, ins_w_id, status") .eq("id", editId) .eq("document_id", documentId) .single(); - console.log(`[edit-resolution] fetched edit row`, { edit, editErr }); + if (editErr) + return void res.status(404).json({ detail: "Edit not found" }); if (!edit) { - console.log(`[edit-resolution] edit not found, returning 404`); return void res.status(404).json({ detail: "Edit not found" }); } // Idempotent: if the edit is already resolved, return the current doc // state so stale UI (e.g. an old chat reloaded in a new session) can // reconcile without throwing. if (edit.status !== "pending") { - console.log(`[edit-resolution] edit already resolved`, { - editId, - status: edit.status, - }); const { data: doc } = await db .from("documents") .select("current_version_id, filename, user_id, project_id") .eq("id", documentId) .single(); if (!doc) { - console.log(`[edit-resolution] doc not found for resolved edit`); return void res.status(404).json({ detail: "Document not found" }); } const accessResolved = await ensureDocAccess(doc, userId, userEmail, db); if (!accessResolved.ok) { - console.log(`[edit-resolution] doc access denied for resolved edit`); return void res.status(404).json({ detail: "Document not found" }); } const activeForResolved = await loadActiveVersion(documentId, db); @@ -685,7 +673,6 @@ async function handleEditResolution( : null, remaining_pending: 0, }; - console.log(`[edit-resolution] returning already-resolved payload`, payload); return void res.status(200).json(payload); } @@ -694,7 +681,8 @@ async function handleEditResolution( .select("id, current_version_id, user_id, project_id") .eq("id", documentId) .single(); - console.log(`[edit-resolution] fetched doc`, { doc, docErr }); + if (docErr) + return void res.status(404).json({ detail: "Document not found" }); if (!doc) return void res.status(404).json({ detail: "Document not found" }); const access = await ensureDocAccess(doc, userId, userEmail, db); @@ -703,17 +691,10 @@ async function handleEditResolution( const active = await loadActiveVersion(documentId, db); const latestPath = active?.storage_path ?? null; - console.log(`[edit-resolution] resolved latestPath`, { - latestPath, - current_version_id: doc.current_version_id, - }); if (!latestPath) return void res.status(404).json({ detail: "No file to edit" }); const raw = await downloadFile(latestPath); - console.log(`[edit-resolution] downloaded bytes`, { - byteLength: raw?.byteLength ?? 0, - }); if (!raw) return void res.status(404).json({ detail: "Document bytes not available" }); @@ -725,24 +706,15 @@ async function handleEditResolution( wIds, mode, ); - console.log(`[edit-resolution] resolveTrackedChange result`, { - mode, - change_id: edit.change_id, - wIds, - found, - resolvedByteLength: resolvedBytes?.byteLength ?? 0, - }); if (!found) { - console.log( - `[edit-resolution] change_id not found in docx — updating status only`, - ); // Still update DB status so the UI reflects the decision — the change // may have been auto-consumed by a previous accept/reject pass. const { error: updErr } = await db .from("document_edits") .update({ status: mode === "accept" ? "accepted" : "rejected", resolved_at: new Date().toISOString() }) .eq("id", editId); - console.log(`[edit-resolution] status-only update`, { updErr }); + if (updErr) + return void res.status(500).json({ detail: "Failed to update edit" }); const { data: filenameRow } = await db .from("documents") .select("filename") @@ -757,7 +729,6 @@ async function handleEditResolution( ), remaining_pending: 0, }; - console.log(`[edit-resolution] returning not-found payload`, payload); return void res.status(200).json(payload); } @@ -770,10 +741,6 @@ async function handleEditResolution( resolvedBytes.byteOffset, resolvedBytes.byteOffset + resolvedBytes.byteLength, ) as ArrayBuffer; - console.log(`[edit-resolution] overwriting bytes in place`, { - latestPath, - byteLength: ab.byteLength, - }); await uploadFile( latestPath, ab, @@ -787,18 +754,14 @@ async function handleEditResolution( resolved_at: new Date().toISOString(), }) .eq("id", editId); - console.log(`[edit-resolution] updated document_edits status`, { - editId, - newStatus: mode === "accept" ? "accepted" : "rejected", - statusErr, - }); + if (statusErr) + return void res.status(500).json({ detail: "Failed to update edit" }); const { count: remainingPending } = await db .from("document_edits") .select("id", { count: "exact", head: true }) .eq("document_id", documentId) .eq("status", "pending"); - console.log(`[edit-resolution] remaining pending count`, { remainingPending }); const { data: filenameRow } = await db .from("documents") @@ -814,7 +777,6 @@ async function handleEditResolution( ), remaining_pending: remainingPending ?? 0, }; - console.log(`[edit-resolution] returning success payload`, payload); res.json(payload); } @@ -830,7 +792,7 @@ documentsRouter.post( (req, res) => void handleEditResolution(req, res, "reject"), ); -async function handleDocumentUpload( +export async function handleDocumentUpload( req: import("express").Request, res: import("express").Response, userId: string, @@ -841,15 +803,15 @@ async function handleDocumentUpload( if (!file) return void res.status(400).json({ detail: "file is required" }); const filename = file.originalname; - const suffix = filename.includes(".") - ? filename.split(".").pop()!.toLowerCase() - : ""; - if (!ALLOWED_TYPES.has(suffix)) - return void res - .status(400) - .json({ - detail: `Unsupported file type: ${suffix}. Allowed: pdf, docx, doc`, - }); + let validated: Awaited>; + try { + validated = await validateDocumentUpload(file); + } catch (err) { + return void res.status(400).json({ + detail: err instanceof Error ? err.message : "Invalid upload", + }); + } + const suffix = validated.suffix; const content = file.buffer; const { data: doc, error: insertErr } = await db @@ -872,17 +834,13 @@ async function handleDocumentUpload( try { const docId = doc.id as string; const key = storageKey(userId, docId, filename); - const contentType = - suffix === "pdf" - ? "application/pdf" - : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; await uploadFile( key, content.buffer.slice( content.byteOffset, content.byteOffset + content.byteLength, ) as ArrayBuffer, - contentType, + validated.contentType, ); const rawBuf = content.buffer.slice( @@ -908,10 +866,7 @@ async function handleDocumentUpload( ); pdfStoragePath = pdfKey; } catch (err) { - console.error( - `[upload] DOCX→PDF conversion failed for ${filename}:`, - err, - ); + console.error("[upload] DOCX→PDF conversion failed", err); } } else if (suffix === "pdf") { pdfStoragePath = key; diff --git a/backend/src/routes/mcpOauth.ts b/backend/src/routes/mcpOauth.ts new file mode 100644 index 00000000..cec60239 --- /dev/null +++ b/backend/src/routes/mcpOauth.ts @@ -0,0 +1,115 @@ +// Unauthenticated OAuth callback for MCP connectors. +// +// The user is bounced here from the connector's authorization server with +// `?code=...&state=...` after consenting in the popup. We don't have an +// auth header here (different origin / popup context), so the route is +// public — the HMAC-signed `state` token carries the user_id + server_id +// we need to find the row. + +import { Router } from "express"; +import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; +import { createServerSupabase } from "../lib/supabase"; +import { DbOAuthProvider, verifyOAuthState } from "../lib/mcp/oauth"; + +export const mcpOauthRouter = Router(); + +const RESULT_HTML = (success: boolean, message?: string) => ` + + + + ${success ? "Connector connected" : "Connector failed"} + + + +

${ + success ? "✓ Connector connected" : "✗ Connection failed" + }

+

${ + success + ? "You can close this window and return to Mike." + : (message ?? "Something went wrong. Close this window and try again.") + }

+ + +`; + +mcpOauthRouter.get("/callback", async (req, res) => { + const code = (req.query.code as string | undefined)?.trim(); + const state = (req.query.state as string | undefined)?.trim(); + const error = (req.query.error as string | undefined)?.trim(); + + if (error) { + return void res + .status(400) + .type("html") + .send(RESULT_HTML(false, `Authorization server returned: ${error}`)); + } + if (!code || !state) { + return void res + .status(400) + .type("html") + .send(RESULT_HTML(false, "Missing code or state.")); + } + + const decoded = verifyOAuthState(state); + if (!decoded) { + return void res + .status(400) + .type("html") + .send(RESULT_HTML(false, "Invalid or expired state — restart sign-in.")); + } + + const db = createServerSupabase(); + const { data: row, error: fetchErr } = await db + .from("user_mcp_servers") + .select("id, user_id, url, auth_type") + .eq("id", decoded.server_id) + .eq("user_id", decoded.user_id) + .single(); + if (fetchErr || !row) { + return void res + .status(404) + .type("html") + .send(RESULT_HTML(false, "Connector not found.")); + } + if (row.auth_type !== "oauth") { + return void res + .status(400) + .type("html") + .send(RESULT_HTML(false, "Connector is not configured for OAuth.")); + } + + const provider = new DbOAuthProvider(db, row.id, row.user_id, "initiate"); + try { + const result = await auth(provider, { + serverUrl: row.url, + authorizationCode: code, + }); + if (result !== "AUTHORIZED") { + throw new Error(`Token exchange returned ${result}`); + } + // saveTokens() ran inside auth() and cleared last_error. + return void res.type("html").send(RESULT_HTML(true)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + await db + .from("user_mcp_servers") + .update({ last_error: message.slice(0, 1000) }) + .eq("id", row.id); + return void res + .status(500) + .type("html") + .send(RESULT_HTML(false, message)); + } +}); diff --git a/backend/src/routes/mcpServers.ts b/backend/src/routes/mcpServers.ts new file mode 100644 index 00000000..f50aede8 --- /dev/null +++ b/backend/src/routes/mcpServers.ts @@ -0,0 +1,416 @@ +// CRUD for user-configurable MCP (Model Context Protocol) servers. +// +// Mounted at `/user/mcp-servers`. The backend uses Supabase's service role +// (bypassing RLS), so every handler MUST filter by `user_id = userId`. + +import net from "net"; +import { Router } from "express"; +import { auth as runOAuth } from "@modelcontextprotocol/sdk/client/auth.js"; +import { requireAuth } from "../middleware/auth"; +import { createServerSupabase } from "../lib/supabase"; +import { McpHttpClient } from "../lib/mcp/client"; +import { DbOAuthProvider } from "../lib/mcp/oauth"; +import { decryptJsonBlob, encryptJsonBlob } from "../lib/apiKeys"; + +export const mcpServersRouter = Router(); + +const SLUG_RE = /^[a-z0-9_-]{1,24}$/; +const NAME_MAX = 80; +const URL_MAX = 500; +const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/; +const MAX_HEADERS = 20; +const MAX_HEADER_VALUE_LEN = 4096; + +type Body = { + name?: unknown; + slug?: unknown; + url?: unknown; + headers?: unknown; + enabled?: unknown; + auth_type?: unknown; +}; + +function deriveSlug(name: string): string { + const base = name + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^[-_]+|[-_]+$/g, "") + .slice(0, 24); + return base || "mcp"; +} + +// Block obvious SSRF targets at submit time: private/reserved IP literals, +// link-local, and single-label hostnames that almost always resolve to +// cluster-internal services (e.g. "postgres", "garage", "redis"). Set +// MCP_ALLOW_PRIVATE_HOSTS=true to bypass — useful for laptop dev where you +// might run an MCP server on a docker service alias. +// +// This is point-in-time validation only; it does not defend against DNS +// rebinding or runtime resolution to a private IP. Closing that loop would +// require per-request DNS resolution + bind-to-IP at fetch time. +function isPrivateIPv4(host: string): boolean { + if (!net.isIPv4(host)) return false; + const parts = host.split(".").map((n) => Number(n)); + return ( + parts[0] === 0 || + parts[0] === 10 || + (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || + (parts[0] === 192 && parts[1] === 168) || + (parts[0] === 169 && parts[1] === 254) || + parts[0] === 127 + ); +} + +function isPrivateIPv6(host: string): boolean { + if (!net.isIPv6(host)) return false; + const lo = host.toLowerCase(); + return ( + lo === "::1" || + lo.startsWith("fc") || + lo.startsWith("fd") || + lo.startsWith("fe80") || + lo === "::" + ); +} + +function validateUrl(raw: string): { ok: true } | { ok: false; error: string } { + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + return { ok: false, error: "url is not a valid URL" }; + } + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { + return { ok: false, error: "url must use http or https" }; + } + const allowPrivate = process.env.MCP_ALLOW_PRIVATE_HOSTS === "true"; + const host = parsed.hostname.toLowerCase(); + if (!allowPrivate) { + if (host === "localhost") { + return { + ok: false, + error: + "localhost is blocked; set MCP_ALLOW_PRIVATE_HOSTS=true for local development", + }; + } + if (isPrivateIPv4(host)) { + return { ok: false, error: `${host} is in a private/reserved IPv4 range` }; + } + if (isPrivateIPv6(host)) { + return { ok: false, error: `${host} is in a private/reserved IPv6 range` }; + } + if (!host.includes(".")) { + return { + ok: false, + error: `single-label hostname "${host}" looks cluster-internal; set MCP_ALLOW_PRIVATE_HOSTS=true if intentional`, + }; + } + } + if (parsed.protocol === "http:" && !allowPrivate) { + return { + ok: false, + error: "url must use https (or set MCP_ALLOW_PRIVATE_HOSTS=true for plaintext localhost development)", + }; + } + return { ok: true }; +} + +function validateHeaders( + raw: unknown, +): { ok: true; value: Record } | { ok: false; error: string } { + if (raw === undefined || raw === null) return { ok: true, value: {} }; + if (typeof raw !== "object" || Array.isArray(raw)) { + return { ok: false, error: "headers must be an object of string→string" }; + } + const entries = Object.entries(raw as Record); + if (entries.length > MAX_HEADERS) { + return { ok: false, error: `headers may not have more than ${MAX_HEADERS} entries` }; + } + const out: Record = {}; + for (const [k, v] of entries) { + if (!HEADER_NAME_RE.test(k)) { + return { ok: false, error: `invalid header name: ${k}` }; + } + if (typeof v !== "string" || v.length > MAX_HEADER_VALUE_LEN) { + return { ok: false, error: `header '${k}' value must be a string of ≤${MAX_HEADER_VALUE_LEN} chars` }; + } + out[k] = v; + } + return { ok: true, value: out }; +} + +function publicShape>(row: T) { + const { + headers, + oauth_metadata: _md, + oauth_tokens: tokens, + oauth_code_verifier: _cv, + ...rest + } = row as T & { + headers?: unknown; + oauth_metadata?: unknown; + oauth_tokens?: unknown; + oauth_code_verifier?: unknown; + }; + // Headers may be either an `enc:v1:` ciphertext string or, for legacy + // rows, the raw plaintext jsonb object — decryptJsonBlob normalizes both + // to a plain object so we can read header names. Token presence is a + // boolean, so we don't even bother decrypting it; non-null is enough. + const decryptedHeaders = + decryptJsonBlob>(headers) ?? {}; + return { + ...rest, + header_keys: Object.keys(decryptedHeaders), + // Boolean only — never round-trip the actual access token to the + // browser, even to the row's owner. + oauth_authorized: tokens !== null && tokens !== undefined, + }; +} + +// GET /user/mcp-servers — list (header values redacted, only keys returned) +mcpServersRouter.get("/", requireAuth, async (_req, res) => { + const userId = res.locals.userId as string; + const db = createServerSupabase(); + const { data, error } = await db + .from("user_mcp_servers") + .select("id, slug, name, url, headers, enabled, last_error, auth_type, oauth_tokens, created_at, updated_at") + .eq("user_id", userId) + .order("created_at", { ascending: true }); + if (error) return void res.status(500).json({ detail: error.message }); + res.json((data ?? []).map(publicShape)); +}); + +// POST /user/mcp-servers — create +mcpServersRouter.post("/", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const body = (req.body ?? {}) as Body; + + const name = typeof body.name === "string" ? body.name.trim() : ""; + if (!name || name.length > NAME_MAX) { + return void res.status(400).json({ detail: `name is required (≤${NAME_MAX} chars)` }); + } + const url = typeof body.url === "string" ? body.url.trim() : ""; + if (!url || url.length > URL_MAX) { + return void res.status(400).json({ detail: `url is required (≤${URL_MAX} chars)` }); + } + const urlOk = validateUrl(url); + if (!urlOk.ok) return void res.status(400).json({ detail: urlOk.error }); + + let slug = typeof body.slug === "string" && body.slug.trim() + ? body.slug.trim().toLowerCase() + : deriveSlug(name); + if (!SLUG_RE.test(slug)) { + return void res.status(400).json({ detail: "slug must match /^[a-z0-9_-]{1,24}$/" }); + } + + const headersOk = validateHeaders(body.headers); + if (!headersOk.ok) return void res.status(400).json({ detail: headersOk.error }); + + const auth_type = + body.auth_type === "oauth" ? "oauth" : "headers"; + + const enabled = body.enabled === false ? false : true; + + const db = createServerSupabase(); + // headers is encrypted-at-rest as an `enc:v1:` string in the jsonb column. + // OAuth-mode rows have no static headers, so we store an empty (encrypted) + // object rather than `null` for shape consistency on read. + const headersToStore = + auth_type === "oauth" ? {} : headersOk.value; + const { data, error } = await db + .from("user_mcp_servers") + .insert({ + user_id: userId, + slug, + name, + url, + headers: encryptJsonBlob(headersToStore) ?? {}, + enabled, + auth_type, + }) + .select("id, slug, name, url, headers, enabled, last_error, auth_type, oauth_tokens, created_at, updated_at") + .single(); + if (error) { + const status = error.code === "23505" ? 409 : 500; + return void res.status(status).json({ detail: error.message }); + } + res.json(publicShape(data)); +}); + +// PATCH /user/mcp-servers/:id — update name/url/headers/enabled +mcpServersRouter.patch("/:id", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const { id } = req.params; + const body = (req.body ?? {}) as Body; + const update: Record = { updated_at: new Date().toISOString() }; + + if (body.name !== undefined) { + const name = typeof body.name === "string" ? body.name.trim() : ""; + if (!name || name.length > NAME_MAX) { + return void res.status(400).json({ detail: `name must be 1–${NAME_MAX} chars` }); + } + update.name = name; + } + if (body.url !== undefined) { + const url = typeof body.url === "string" ? body.url.trim() : ""; + if (!url) return void res.status(400).json({ detail: "url is required" }); + const urlOk = validateUrl(url); + if (!urlOk.ok) return void res.status(400).json({ detail: urlOk.error }); + update.url = url; + // Changing the URL invalidates every credential that was negotiated + // for the previous origin. Without these clears, the next call would + // send the old server's bearer/refresh tokens to the new authority — + // a token leak. Re-running OAuth (or re-supplying headers) is required. + update.oauth_tokens = null; + update.oauth_metadata = null; + update.oauth_code_verifier = null; + update.headers = encryptJsonBlob({}) ?? {}; + } + if (body.headers !== undefined) { + const headersOk = validateHeaders(body.headers); + if (!headersOk.ok) return void res.status(400).json({ detail: headersOk.error }); + update.headers = encryptJsonBlob(headersOk.value) ?? {}; + } + if (body.enabled !== undefined) { + update.enabled = body.enabled === true; + } + + const db = createServerSupabase(); + const { data, error } = await db + .from("user_mcp_servers") + .update(update) + .eq("id", id) + .eq("user_id", userId) + .select("id, slug, name, url, headers, enabled, last_error, auth_type, oauth_tokens, created_at, updated_at") + .single(); + if (error || !data) { + return void res.status(404).json({ detail: error?.message ?? "Not found" }); + } + res.json(publicShape(data)); +}); + +// DELETE /user/mcp-servers/:id +mcpServersRouter.delete("/:id", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const { id } = req.params; + const db = createServerSupabase(); + const { error } = await db + .from("user_mcp_servers") + .delete() + .eq("id", id) + .eq("user_id", userId); + if (error) return void res.status(500).json({ detail: error.message }); + res.status(204).send(); +}); + +// POST /user/mcp-servers/:id/test — connect + list_tools, return summary +mcpServersRouter.post("/:id/test", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const { id } = req.params; + const db = createServerSupabase(); + const { data: row, error } = await db + .from("user_mcp_servers") + .select("url, headers, auth_type, oauth_tokens") + .eq("id", id) + .eq("user_id", userId) + .single(); + if (error || !row) { + return void res.status(404).json({ detail: "Not found" }); + } + + if (row.auth_type === "oauth" && !row.oauth_tokens) { + return void res.status(200).json({ + ok: false, + error: "Connector is configured for OAuth but not yet signed in.", + }); + } + + const provider = + row.auth_type === "oauth" + ? new DbOAuthProvider(db, id, userId, "use") + : undefined; + // headers may be encrypted-at-rest; decryptJsonBlob handles both the + // ciphertext and legacy plaintext-jsonb cases transparently. + const decryptedHeaders = + decryptJsonBlob>(row.headers) ?? {}; + const client = new McpHttpClient( + row.url, + decryptedHeaders, + provider, + ); + try { + await client.connect(); + const tools = await client.listTools(); + await db + .from("user_mcp_servers") + .update({ last_error: null }) + .eq("id", id); + res.json({ + ok: true, + tool_count: tools.length, + tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })), + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + await db + .from("user_mcp_servers") + .update({ last_error: message.slice(0, 1000) }) + .eq("id", id); + res.status(200).json({ ok: false, error: message }); + } finally { + await client.close(); + } +}); + +// POST /user/mcp-servers/:id/oauth/start — discover + DCR + build authorize URL +// +// Returns { authorize_url } so the frontend can open it in a popup. The user +// completes consent at the connector's auth server and is redirected back to +// /mcp/oauth/callback (mounted under mcpOauthRouter), which exchanges the +// code and stores tokens. +mcpServersRouter.post("/:id/oauth/start", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const { id } = req.params; + const db = createServerSupabase(); + const { data: row, error } = await db + .from("user_mcp_servers") + .select("id, user_id, url, auth_type") + .eq("id", id) + .eq("user_id", userId) + .single(); + if (error || !row) return void res.status(404).json({ detail: "Not found" }); + if (row.auth_type !== "oauth") { + return void res + .status(400) + .json({ detail: "Connector is not configured for OAuth" }); + } + + const provider = new DbOAuthProvider(db, row.id, userId, "initiate"); + try { + const result = await runOAuth(provider, { serverUrl: row.url }); + if (result === "AUTHORIZED") { + // Already valid (e.g. row had a usable refresh token). Nothing + // for the user to do. + return void res.json({ + authorize_url: null, + already_authorized: true, + }); + } + if (!provider.lastAuthorizeUrl) { + throw new Error("Auth flow returned REDIRECT but no URL"); + } + await db + .from("user_mcp_servers") + .update({ last_error: null }) + .eq("id", id); + res.json({ authorize_url: provider.lastAuthorizeUrl.toString() }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + await db + .from("user_mcp_servers") + .update({ last_error: message.slice(0, 1000) }) + .eq("id", id); + res.status(500).json({ detail: message }); + } +}); diff --git a/backend/src/routes/projectChat.ts b/backend/src/routes/projectChat.ts index 5e299615..35129a67 100644 --- a/backend/src/routes/projectChat.ts +++ b/backend/src/routes/projectChat.ts @@ -13,6 +13,10 @@ import { } from "../lib/chatTools"; import { getUserApiKeys } from "../lib/userSettings"; import { checkProjectAccess } from "../lib/access"; +import { + closeMcpServers, + loadEnabledMcpServersForUser, +} from "../lib/mcp/servers"; const PROJECT_SYSTEM_PROMPT_EXTRA = `PROJECT CONTEXT: You are operating within a project folder that contains a collection of legal documents the user has organised for a single matter. The user's questions will usually refer to one or more documents in this project — your job is to find the relevant files to work on. Use list_documents to see what is available and fetch_documents / read_document to pull in any documents you need before answering. @@ -153,6 +157,7 @@ projectChatRouter.post("/", requireAuth, async (req, res) => { const write = (line: string) => res.write(line); const apiKeys = await getUserApiKeys(userId, db); + const mcpServers = await loadEnabledMcpServersForUser(userId, db); try { write(`data: ${JSON.stringify({ type: "chat_id", chatId })}\n\n`); @@ -169,6 +174,7 @@ projectChatRouter.post("/", requireAuth, async (req, res) => { model, apiKeys, projectId, + mcpServers, }); const annotations = extractAnnotations(fullText, docIndex, events); @@ -196,6 +202,7 @@ projectChatRouter.post("/", requireAuth, async (req, res) => { /* ignore */ } } finally { + await closeMcpServers(mcpServers); res.end(); } }); diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 61fdd83e..7205bf85 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -10,14 +10,18 @@ import { downloadFile, uploadFile, storageKey } from "../lib/storage"; import { docxToPdf, convertedPdfKey } from "../lib/convert"; import { checkProjectAccess } from "../lib/access"; import { singleFileUpload } from "../lib/upload"; +import { handleDocumentUpload } from "./documents"; export const projectsRouter = Router(); -const ALLOWED_TYPES = new Set(["pdf", "docx", "doc"]); // GET /projects projectsRouter.get("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; - const userEmail = res.locals.userEmail as string; + // Stored shared_with values are normalised to lowercase on PATCH/POST, + // so the lookup must lowercase the JWT email too — Google/Microsoft/etc. + // can issue tokens with mixed-case emails and a mismatch silently makes + // the row invisible. + const userEmail = (res.locals.userEmail as string | undefined)?.toLowerCase(); const db = createServerSupabase(); const { data: ownProjects, error: ownError } = await db @@ -100,7 +104,7 @@ projectsRouter.post("/", requireAuth, async (req, res) => { // GET /projects/:projectId projectsRouter.get("/:projectId", requireAuth, async (req, res) => { const userId = res.locals.userId as string; - const userEmail = res.locals.userEmail as string; + const userEmail = (res.locals.userEmail as string | undefined)?.toLowerCase(); const { projectId } = req.params; const db = createServerSupabase(); @@ -526,11 +530,14 @@ projectsRouter.patch("/:projectId/folders/:folderId", requireAuth, async (req, r if ("parent_folder_id" in body) { // Cycle check: walk up the tree from the proposed parent to ensure folderId is not an ancestor if (body.parent_folder_id) { + const parent = await loadProjectFolder(db, projectId, body.parent_folder_id); + if (!parent) return void res.status(404).json({ detail: "Parent folder not found" }); + let cur: string | null = body.parent_folder_id; while (cur) { if (cur === folderId) return void res.status(400).json({ detail: "Cannot move a folder into itself or a descendant" }); - const { data: p }: { data: { parent_folder_id: string | null } | null } = - await db.from("project_subfolders").select("parent_folder_id").eq("id", cur).single(); + const p = await loadProjectFolder(db, projectId, cur); + if (!p) return void res.status(404).json({ detail: "Parent folder not found" }); cur = p?.parent_folder_id ?? null; } } @@ -555,8 +562,11 @@ projectsRouter.delete("/:projectId/folders/:folderId", requireAuth, async (req, const access = await checkProjectAccess(projectId, userId, userEmail, db); if (!access.ok) return void res.status(404).json({ detail: "Project not found" }); + const folder = await loadProjectFolder(db, projectId, folderId); + if (!folder) return void res.status(404).json({ detail: "Folder not found" }); + // Move direct documents to root before cascade-deleting subfolders - await db.from("documents").update({ folder_id: null }).eq("folder_id", folderId); + await db.from("documents").update({ folder_id: null }).eq("folder_id", folderId).eq("project_id", projectId); const { error } = await db.from("project_subfolders") .delete().eq("id", folderId).eq("project_id", projectId); @@ -575,6 +585,11 @@ projectsRouter.patch("/:projectId/documents/:documentId/folder", requireAuth, as const access = await checkProjectAccess(projectId, userId, userEmail, db); if (!access.ok) return void res.status(404).json({ detail: "Project not found" }); + if (folder_id) { + const folder = await loadProjectFolder(db, projectId, folder_id); + if (!folder) return void res.status(404).json({ detail: "Folder not found" }); + } + const { data, error } = await db.from("documents") .update({ folder_id: folder_id ?? null, updated_at: new Date().toISOString() }) .eq("id", documentId).eq("project_id", projectId) @@ -583,219 +598,16 @@ projectsRouter.patch("/:projectId/documents/:documentId/folder", requireAuth, as res.json(data); }); -export async function handleDocumentUpload( - req: import("express").Request, - res: import("express").Response, - userId: string, - projectId: string | null, +async function loadProjectFolder( db: ReturnType, -) { - const file = req.file; - if (!file) return void res.status(400).json({ detail: "file is required" }); - - const filename = file.originalname; - const suffix = filename.includes(".") - ? filename.split(".").pop()!.toLowerCase() - : ""; - if (!ALLOWED_TYPES.has(suffix)) - return void res - .status(400) - .json({ - detail: `Unsupported file type: ${suffix}. Allowed: pdf, docx, doc`, - }); - - const content = file.buffer; - const { data: doc, error: insertErr } = await db - .from("documents") - .insert({ - project_id: projectId, - user_id: userId, - filename, - file_type: suffix, - size_bytes: content.byteLength, - status: "processing", - }) - .select("*") - .single(); - - if (insertErr || !doc) - return void res - .status(500) - .json({ detail: "Failed to create document record" }); - - try { - const docId = doc.id as string; - const key = storageKey(userId, docId, filename); - const contentType = - suffix === "pdf" - ? "application/pdf" - : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - await uploadFile( - key, - content.buffer.slice( - content.byteOffset, - content.byteOffset + content.byteLength, - ) as ArrayBuffer, - contentType, - ); - - const rawBuf = content.buffer.slice( - content.byteOffset, - content.byteOffset + content.byteLength, - ) as ArrayBuffer; - const tree = await extractStructureTree(rawBuf, suffix, filename); - const pageCount = suffix === "pdf" ? await countPdfPages(rawBuf) : null; - - // Convert DOCX/DOC → PDF for display. PDFs are their own rendition. - let pdfStoragePath: string | null = null; - if (suffix === "docx" || suffix === "doc") { - try { - const pdfBuf = await docxToPdf(content); - const pdfKey = convertedPdfKey(userId, docId); - await uploadFile( - pdfKey, - pdfBuf.buffer.slice( - pdfBuf.byteOffset, - pdfBuf.byteOffset + pdfBuf.byteLength, - ) as ArrayBuffer, - "application/pdf", - ); - pdfStoragePath = pdfKey; - } catch (err) { - console.error( - `[upload] DOCX→PDF conversion failed for ${filename}:`, - err, - ); - } - } else if (suffix === "pdf") { - pdfStoragePath = key; - } - - // Storage paths live on document_versions — create the V1 row and - // point documents.current_version_id at it. - const { data: versionRow, error: verErr } = await db - .from("document_versions") - .insert({ - document_id: docId, - storage_path: key, - pdf_storage_path: pdfStoragePath, - source: "upload", - version_number: 1, - display_name: filename, - }) - .select("id") - .single(); - if (verErr || !versionRow) { - throw new Error( - `Failed to record upload version: ${verErr?.message ?? "unknown"}`, - ); - } - - await db - .from("documents") - .update({ - current_version_id: versionRow.id, - size_bytes: content.byteLength, - page_count: pageCount, - structure_tree: tree ?? null, - status: "ready", - updated_at: new Date().toISOString(), - }) - .eq("id", docId); - - const { data: updated } = await db - .from("documents") - .select("*") - .eq("id", docId) - .single(); - const responseDoc = updated - ? { - ...updated, - storage_path: key, - pdf_storage_path: pdfStoragePath, - } - : updated; - return void res.status(201).json(responseDoc); - } catch (e) { - await db.from("documents").update({ status: "error" }).eq("id", doc.id); - return void res - .status(500) - .json({ detail: `Document processing failed: ${String(e)}` }); - } -} - -async function countPdfPages(buf: ArrayBuffer): Promise { - try { - const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs" as string); - const pdf = await ( - pdfjsLib as unknown as { - getDocument: (opts: unknown) => { - promise: Promise<{ numPages: number }>; - }; - } - ).getDocument({ data: new Uint8Array(buf) }).promise; - return pdf.numPages; - } catch { - return null; - } -} - -async function extractStructureTree( - content: ArrayBuffer, - fileType: string, - filename: string, -): Promise { - try { - if (fileType === "pdf") { - const pdfjsLib = await import( - "pdfjs-dist/legacy/build/pdf.mjs" as string - ); - const pdf = await ( - pdfjsLib as unknown as { - getDocument: (opts: unknown) => { - promise: Promise<{ - numPages: number; - getOutline: () => Promise<{ title?: string }[]>; - }>; - }; - } - ).getDocument({ data: new Uint8Array(content) }).promise; - if (pdf.numPages <= 5) return null; - const outline = await pdf.getOutline(); - if (outline?.length) { - return outline.map((item, i) => ({ - id: `h1-${i}`, - title: item.title ?? `Item ${i + 1}`, - level: 1, - page_number: null, - children: [], - })); - } - return Array.from({ length: pdf.numPages }, (_, i) => ({ - id: `page-${i + 1}`, - title: `Page ${i + 1}`, - level: 1, - page_number: i + 1, - children: [], - })); - } else { - const mammoth = await import("mammoth"); - const result = await mammoth.extractRawText({ - buffer: Buffer.from(content), - }); - const lines = result.value.split("\n").filter((l) => l.trim()); - const nodes = lines - .slice(0, 30) - .map((line, i) => ({ - id: `h1-${i}`, - title: line.slice(0, 100), - level: 1, - page_number: null, - children: [], - })); - return nodes.length ? nodes : null; - } - } catch { - return null; - } + projectId: string, + folderId: string, +): Promise<{ id: string; parent_folder_id: string | null } | null> { + const { data } = await db + .from("project_subfolders") + .select("id, parent_folder_id") + .eq("id", folderId) + .eq("project_id", projectId) + .maybeSingle(); + return (data as { id: string; parent_folder_id: string | null } | null) ?? null; } diff --git a/backend/src/routes/tabular.ts b/backend/src/routes/tabular.ts index 2b4f6db9..3ec67741 100644 --- a/backend/src/routes/tabular.ts +++ b/backend/src/routes/tabular.ts @@ -13,6 +13,7 @@ import { import { completeText, streamChatWithTools } from "../lib/llm"; import { getUserApiKeys, getUserModelSettings } from "../lib/userSettings"; import { + canEditReview, checkProjectAccess, ensureReviewAccess, listAccessibleProjectIds, @@ -45,6 +46,82 @@ function formatPromptSuffix(format?: string, tags?: string[]): string { export const tabularRouter = Router(); +type Db = ReturnType; +type ReviewAccessRow = { + id: string; + user_id: string; + project_id: string | null; + shared_with?: string[] | null; +}; +type ReviewDocumentRow = { + id: string; + filename: string; + file_type?: string | null; + page_count?: number | null; + user_id: string; + project_id: string | null; +}; + +async function loadAuthorizedReviewDocuments( + review: ReviewAccessRow, + documentIds: string[], + db: Db, +): Promise { + const ids = [...new Set(documentIds.filter((id) => typeof id === "string" && id))]; + if (ids.length === 0) return []; + const { data } = await db + .from("documents") + .select("id, filename, file_type, page_count, user_id, project_id") + .in("id", ids); + const docs = ((data ?? []) as ReviewDocumentRow[]).filter((doc) => { + if (review.project_id) return doc.project_id === review.project_id; + return doc.user_id === review.user_id; + }); + const order = new Map(ids.map((id, index) => [id, index])); + return docs.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0)); +} + +export async function validateReviewDocumentIds( + review: ReviewAccessRow, + documentIds: string[], + db: Db, +): Promise<{ ok: true; documentIds: string[] } | { ok: false; detail: string }> { + const ids = [...new Set(documentIds.filter((id) => typeof id === "string" && id))]; + if (ids.length !== documentIds.length) { + return { ok: false, detail: "document_ids contains invalid values" }; + } + const docs = await loadAuthorizedReviewDocuments(review, ids, db); + if (docs.length !== ids.length) { + return { ok: false, detail: "One or more documents are not available for this review" }; + } + return { ok: true, documentIds: ids }; +} + +async function validateNewReviewDocumentIds( + args: { + userId: string; + projectId: string | null; + documentIds: string[]; + db: Db; + }, +): Promise<{ ok: true; documentIds: string[] } | { ok: false; detail: string }> { + const reviewLike: ReviewAccessRow = { + id: "new", + user_id: args.userId, + project_id: args.projectId, + }; + return validateReviewDocumentIds(reviewLike, args.documentIds, args.db); +} + +function requireReviewEdit( + access: { ok: true; isOwner: boolean; via?: "owner" | "project" | "direct" }, + res: import("express").Response, +): boolean { + if (canEditReview(access)) return true; + res.status(403).json({ detail: "Review is read-only for this user" }); + return false; +} + // GET /tabular-review tabularRouter.get("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; @@ -104,7 +181,18 @@ tabularRouter.get("/", requireAuth, async (req, res) => { ? db .from("tabular_reviews") .select("*") - .contains("shared_with", JSON.stringify([userEmail])) + // Two fixes here vs. the previous form: + // 1. shared_with is jsonb, so `.contains` needs the JSON + // array literal (`["foo@bar"]`), not the Postgres array + // literal (`{foo@bar}`) — bare arrays produced + // "invalid input syntax for type json" 400s, swallowed + // by the catch below as if the column hadn't migrated. + // 2. Lowercase the JWT email so providers issuing mixed-case + // emails still match the (lowercased on insert) rows. + .contains( + "shared_with", + JSON.stringify([userEmail.toLowerCase()]), + ) .neq("user_id", userId) .order("created_at", { ascending: false }) : Promise.resolve({ @@ -192,6 +280,18 @@ tabularRouter.post("/", requireAuth, async (req, res) => { if (!access.ok) return void res.status(404).json({ detail: "Project not found" }); } + const requestedDocumentIds = Array.isArray(document_ids) + ? document_ids + : []; + const documentValidation = await validateNewReviewDocumentIds({ + userId, + projectId: project_id ?? null, + documentIds: requestedDocumentIds, + db, + }); + if (!documentValidation.ok) { + return void res.status(400).json({ detail: documentValidation.detail }); + } const { data: review, error } = await db .from("tabular_reviews") .insert({ @@ -208,7 +308,7 @@ tabularRouter.post("/", requireAuth, async (req, res) => { .status(500) .json({ detail: error?.message ?? "Failed to create review" }); - const cells = document_ids.flatMap((docId) => + const cells = documentValidation.documentIds.flatMap((docId) => columns_config.map((col) => ({ review_id: review.id, document_id: docId, @@ -315,24 +415,27 @@ tabularRouter.get("/:reviewId", requireAuth, async (req, res) => { .select("*") .eq("review_id", reviewId); const docIds = [...new Set((cells ?? []).map((c) => c.document_id))]; - const docsResult = + const docs = docIds.length > 0 - ? await db.from("documents").select("*").in("id", docIds) + ? await loadAuthorizedReviewDocuments(review as ReviewAccessRow, docIds, db) : review.project_id - ? await db + ? ((await db .from("documents") - .select("*") + .select("id, filename, file_type, page_count, user_id, project_id") .eq("project_id", review.project_id) - .order("created_at", { ascending: true }) - : { data: [] as Record[] }; + .order("created_at", { ascending: true })).data ?? []) as ReviewDocumentRow[] + : []; + const allowedDocIds = new Set(docs.map((doc) => doc.id)); res.json({ review: { ...review, is_owner: access.isOwner }, - cells: (cells ?? []).map((cell) => ({ - ...cell, - content: parseCellContent(cell.content), - })), - documents: docsResult.data ?? [], + cells: (cells ?? []) + .filter((cell) => allowedDocIds.has(cell.document_id)) + .map((cell) => ({ + ...cell, + content: parseCellContent(cell.content), + })), + documents: docs, }); }); @@ -461,6 +564,7 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { ); if (!access.ok) return void res.status(404).json({ detail: "Review not found" }); + if (!requireReviewEdit(access, res)) return; if (sharedWithUpdate !== undefined) { if (!access.isOwner) return void res @@ -468,6 +572,64 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { .json({ detail: "Only the review owner can change sharing" }); updates.shared_with = sharedWithUpdate; } + const nextProjectId = + req.body.project_id !== undefined + ? ((req.body.project_id as string | null) ?? null) + : ((existingReview.project_id as string | null) ?? null); + if (nextProjectId) { + const projectAccess = await checkProjectAccess( + nextProjectId, + userId, + userEmail, + db, + ); + if (!projectAccess.ok) + return void res.status(404).json({ detail: "Project not found" }); + } + if (req.body.project_id !== undefined) { + const { data: existingCellsForProjectMove } = await db + .from("tabular_cells") + .select("document_id") + .eq("review_id", reviewId); + const validation = await validateReviewDocumentIds( + { + id: reviewId, + user_id: existingReview.user_id as string, + project_id: nextProjectId, + shared_with: existingReview.shared_with as string[] | null, + }, + [ + ...new Set( + (existingCellsForProjectMove ?? []).map( + (cell) => cell.document_id as string, + ), + ), + ], + db, + ); + if (!validation.ok) + return void res.status(400).json({ detail: validation.detail }); + } + let requestedDocumentIdsValidation: + | { ok: true; documentIds: string[] } + | { ok: false; detail: string } + | null = null; + if (Array.isArray(req.body.document_ids)) { + requestedDocumentIdsValidation = await validateReviewDocumentIds( + { + id: reviewId, + user_id: existingReview.user_id as string, + project_id: nextProjectId, + shared_with: existingReview.shared_with as string[] | null, + }, + req.body.document_ids as string[], + db, + ); + if (!requestedDocumentIdsValidation.ok) + return void res + .status(400) + .json({ detail: requestedDocumentIdsValidation.detail }); + } const { data: updatedReview, error: updateError } = await db .from("tabular_reviews") @@ -498,12 +660,16 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { if (Array.isArray(req.body.document_ids)) { // document_ids is the new source of truth — delete removed docs' cells - const newDocIds = req.body.document_ids as string[]; + const validation = requestedDocumentIdsValidation; + if (!validation?.ok) + return void res + .status(400) + .json({ detail: "document_ids is invalid" }); const existingDocIds = (existingCells ?? []).map( (cell) => cell.document_id, ); const removedDocIds = existingDocIds.filter( - (id) => !newDocIds.includes(id), + (id) => !validation.documentIds.includes(id), ); if (removedDocIds.length > 0) { @@ -518,7 +684,7 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { .json({ detail: deleteError.message }); } - documentIds = newDocIds; + documentIds = validation.documentIds; } else { // No document change — derive from existing cells documentIds = [ @@ -526,11 +692,11 @@ tabularRouter.patch("/:reviewId", requireAuth, async (req, res) => { (existingCells ?? []).map((cell) => cell.document_id), ), ]; - if (documentIds.length === 0 && existingReview.project_id) { + if (documentIds.length === 0 && nextProjectId) { const { data: projectDocs } = await db .from("documents") .select("id") - .eq("project_id", existingReview.project_id); + .eq("project_id", nextProjectId); documentIds = (projectDocs ?? []).map((doc) => doc.id); } } @@ -605,12 +771,20 @@ tabularRouter.post("/:reviewId/clear-cells", requireAuth, async (req, res) => { const access = await ensureReviewAccess(review, userId, userEmail, db); if (!access.ok) return void res.status(404).json({ detail: "Review not found" }); + if (!requireReviewEdit(access, res)) return; + const validation = await validateReviewDocumentIds( + review as ReviewAccessRow, + document_ids, + db, + ); + if (!validation.ok) + return void res.status(400).json({ detail: validation.detail }); const { error } = await db .from("tabular_cells") .update({ content: null, status: "pending" }) .eq("review_id", reviewId) - .in("document_id", document_ids); + .in("document_id", validation.documentIds); if (error) return void res.status(500).json({ detail: error.message }); res.status(204).send(); }); @@ -644,6 +818,7 @@ tabularRouter.post( const access = await ensureReviewAccess(review, userId, userEmail, db); if (!access.ok) return void res.status(404).json({ detail: "Review not found" }); + if (!requireReviewEdit(access, res)) return; const column = ( review.columns_config as { @@ -657,6 +832,24 @@ tabularRouter.post( if (!column) return void res.status(400).json({ detail: "Column not found" }); + const { data: cell } = await db + .from("tabular_cells") + .select("id") + .eq("review_id", reviewId) + .eq("document_id", document_id) + .eq("column_index", column_index) + .maybeSingle(); + if (!cell) + return void res.status(404).json({ detail: "Cell not found" }); + + const validation = await validateReviewDocumentIds( + review as ReviewAccessRow, + [document_id], + db, + ); + if (!validation.ok) + return void res.status(404).json({ detail: "Document not found" }); + const { data: doc } = await db .from("documents") .select("id, filename, file_type") @@ -743,6 +936,7 @@ tabularRouter.post("/:reviewId/generate", requireAuth, async (req, res) => { const access = await ensureReviewAccess(review, userId, userEmail, db); if (!access.ok) return void res.status(404).json({ detail: "Review not found" }); + if (!requireReviewEdit(access, res)) return; const columns: { index: number; @@ -763,20 +957,20 @@ tabularRouter.post("/:reviewId/generate", requireAuth, async (req, res) => { cellMap.set(`${cell.document_id}:${cell.column_index}`, cell); const docIds = [...new Set((cells ?? []).map((c) => c.document_id))]; - let docs: Record[] = []; + let docs: ReviewDocumentRow[] = []; if (docIds.length > 0) { - const { data } = await db - .from("documents") - .select("id, filename, file_type, page_count") - .in("id", docIds); - docs = data ?? []; + docs = await loadAuthorizedReviewDocuments( + review as ReviewAccessRow, + docIds, + db, + ); } else if (review.project_id) { const { data } = await db .from("documents") - .select("id, filename, file_type, page_count") + .select("id, filename, file_type, page_count, user_id, project_id") .eq("project_id", review.project_id) .order("created_at", { ascending: true }); - docs = data ?? []; + docs = (data ?? []) as ReviewDocumentRow[]; } const { tabular_model, api_keys } = await getUserModelSettings(userId, db); @@ -1148,13 +1342,15 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => { ]; let docs: { id: string; filename: string }[] = []; if (docIds.length > 0) { - const { data } = await db - .from("documents") - .select("id, filename") - .in("id", docIds) - .order("created_at", { ascending: true }); - docs = (data ?? []) as { id: string; filename: string }[]; + docs = ( + await loadAuthorizedReviewDocuments( + review as ReviewAccessRow, + docIds, + db, + ) + ).map((doc) => ({ id: doc.id, filename: doc.filename })); } + const allowedDocIds = new Set(docs.map((doc) => doc.id)); const sortedColumns = ( (review.columns_config ?? []) as { index: number; name: string }[] @@ -1164,10 +1360,12 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => { columns: sortedColumns, documents: docs, cells: new Map( - (cells ?? []).map((c: any) => [ - `${c.column_index}:${c.document_id}`, - parseCellContent(c.content), - ]), + (cells ?? []) + .filter((c: any) => allowedDocIds.has(c.document_id)) + .map((c: any) => [ + `${c.column_index}:${c.document_id}`, + parseCellContent(c.content), + ]), ), }; @@ -1185,9 +1383,7 @@ tabularRouter.post("/:reviewId/chat", requireAuth, async (req, res) => { .select("id, title, review_id, user_id") .eq("id", chatId) .single(); - const canUse = - !!existing && - (existing.review_id === reviewId || existing.user_id === userId); + const canUse = !!existing && existing.review_id === reviewId; if (!canUse || !existing) chatId = null; else chatTitle = existing.title; } diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index aeddd3ad..0d30c472 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -1,23 +1,130 @@ import { Router } from "express"; import { requireAuth } from "../middleware/auth"; import { createServerSupabase } from "../lib/supabase"; +import { encryptApiKeyInputs, hasStoredApiKey } from "../lib/apiKeys"; +import { resolveModel, DEFAULT_TABULAR_MODEL } from "../lib/llm"; export const userRouter = Router(); -// POST /user/profile -userRouter.post("/profile", requireAuth, async (req, res) => { - const userId = res.locals.userId as string; - const db = createServerSupabase(); - const { error } = await db +type ProfileRow = { + display_name: string | null; + organisation: string | null; + message_credits_used: number; + credits_reset_date: string; + tier: string; + tabular_model: string; + claude_api_key: string | null; + gemini_api_key: string | null; + openrouter_api_key: string | null; +}; + +const PROFILE_COLUMNS = + "display_name, organisation, message_credits_used, credits_reset_date, tier, tabular_model, claude_api_key, gemini_api_key, openrouter_api_key"; + +function safeProfile(row: ProfileRow) { + return { + display_name: row.display_name, + organisation: row.organisation, + message_credits_used: row.message_credits_used, + credits_reset_date: row.credits_reset_date, + tier: row.tier, + tabular_model: resolveModel(row.tabular_model, DEFAULT_TABULAR_MODEL), + has_claude_api_key: hasStoredApiKey(row.claude_api_key), + has_gemini_api_key: hasStoredApiKey(row.gemini_api_key), + has_openrouter_api_key: hasStoredApiKey(row.openrouter_api_key), + }; +} + +async function ensureProfile( + userId: string, + db: ReturnType, +) { + await db .from("user_profiles") .upsert( { user_id: userId }, { onConflict: "user_id", ignoreDuplicates: true }, ); +} + +// POST /user/profile +userRouter.post("/profile", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const db = createServerSupabase(); + const { error } = await db.from("user_profiles").upsert( + { user_id: userId }, + { onConflict: "user_id", ignoreDuplicates: true }, + ); if (error) return void res.status(500).json({ detail: error.message }); res.json({ ok: true }); }); +// GET /user/profile +userRouter.get("/profile", requireAuth, async (_req, res) => { + const userId = res.locals.userId as string; + const db = createServerSupabase(); + await ensureProfile(userId, db); + const { data, error } = await db + .from("user_profiles") + .select(PROFILE_COLUMNS) + .eq("user_id", userId) + .single(); + if (error || !data) + return void res + .status(500) + .json({ detail: error?.message ?? "Profile not found" }); + res.json(safeProfile(data as ProfileRow)); +}); + +// PATCH /user/profile +userRouter.patch("/profile", requireAuth, async (req, res) => { + const userId = res.locals.userId as string; + const db = createServerSupabase(); + await ensureProfile(userId, db); + + const updates: Record = {}; + if (typeof req.body.display_name === "string") { + updates.display_name = req.body.display_name.trim().slice(0, 200) || null; + } + if (typeof req.body.organisation === "string") { + updates.organisation = req.body.organisation.trim().slice(0, 200) || null; + } + if (typeof req.body.tabular_model === "string") { + updates.tabular_model = resolveModel( + req.body.tabular_model, + DEFAULT_TABULAR_MODEL, + ); + } + if (req.body.api_keys && typeof req.body.api_keys === "object") { + Object.assign(updates, encryptApiKeyInputs(req.body.api_keys as Record)); + } + if (req.body.increment_message_credits === true) { + const { data: current } = await db + .from("user_profiles") + .select("message_credits_used") + .eq("user_id", userId) + .single(); + updates.message_credits_used = + ((current?.message_credits_used as number | null) ?? 0) + 1; + } + + if (Object.keys(updates).length === 0) { + return void res.status(400).json({ detail: "No supported fields to update" }); + } + + const { data, error } = await db + .from("user_profiles") + .update({ ...updates, updated_at: new Date().toISOString() }) + .eq("user_id", userId) + .select(PROFILE_COLUMNS) + .single(); + if (error || !data) + return void res + .status(500) + .json({ detail: error?.message ?? "Failed to update profile" }); + res.json(safeProfile(data as ProfileRow)); +}); + // DELETE /user/account userRouter.delete("/account", requireAuth, async (_req, res) => { const userId = res.locals.userId as string; diff --git a/backend/test/access.test.ts b/backend/test/access.test.ts new file mode 100644 index 00000000..4ee34f3b --- /dev/null +++ b/backend/test/access.test.ts @@ -0,0 +1,121 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { canEditReview, ensureReviewAccess } from "../src/lib/access"; +import { validateReviewDocumentIds } from "../src/routes/tabular"; + +describe("review access helpers", () => { + it("treats direct standalone review shares as read-only", () => { + assert.equal( + canEditReview({ ok: true, isOwner: false, via: "direct" }), + false, + ); + assert.equal( + canEditReview({ ok: true, isOwner: false, via: "project" }), + true, + ); + assert.equal( + canEditReview({ ok: true, isOwner: true, via: "owner" }), + true, + ); + }); + + it("validates review document IDs against project or owner scope", async () => { + const docs = [ + { + id: "project-doc", + filename: "Project.docx", + user_id: "owner-a", + project_id: "project-a", + }, + { + id: "other-project-doc", + filename: "Other.docx", + user_id: "owner-a", + project_id: "project-b", + }, + { + id: "standalone-doc", + filename: "Standalone.docx", + user_id: "owner-a", + project_id: null, + }, + ]; + const db = { + from: () => ({ + select() { + return this; + }, + in(_column: string, ids: string[]) { + return Promise.resolve({ + data: docs.filter((doc) => ids.includes(doc.id)), + }); + }, + }), + } as never; + + assert.deepEqual( + await validateReviewDocumentIds( + { + id: "review-a", + user_id: "owner-a", + project_id: "project-a", + }, + ["project-doc"], + db, + ), + { ok: true, documentIds: ["project-doc"] }, + ); + assert.equal( + ( + await validateReviewDocumentIds( + { + id: "review-a", + user_id: "owner-a", + project_id: "project-a", + }, + ["other-project-doc"], + db, + ) + ).ok, + false, + ); + assert.deepEqual( + await validateReviewDocumentIds( + { id: "review-b", user_id: "owner-a", project_id: null }, + ["standalone-doc"], + db, + ), + { ok: true, documentIds: ["standalone-doc"] }, + ); + }); + + it("does not let direct email shares bypass project review access", async () => { + const db = { + from: () => ({ + select() { + return this; + }, + eq() { + return this; + }, + single() { + return Promise.resolve({ data: null }); + }, + }), + } as never; + + assert.deepEqual( + await ensureReviewAccess( + { + user_id: "owner-a", + project_id: "project-a", + shared_with: ["viewer@example.com"], + }, + "viewer-user-id", + "viewer@example.com", + db, + ), + { ok: false }, + ); + }); +}); diff --git a/backend/test/apiKeys.test.ts b/backend/test/apiKeys.test.ts new file mode 100644 index 00000000..7895e76d --- /dev/null +++ b/backend/test/apiKeys.test.ts @@ -0,0 +1,163 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + decryptApiKey, + decryptJsonBlob, + encryptApiKey, + encryptJsonBlob, + hasStoredApiKey, + isEncryptedApiKey, + needsJsonBlobUpgrade, +} from "../src/lib/apiKeys"; + +describe("user API key encryption", () => { + it("encrypts and decrypts stored keys", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + const encrypted = encryptApiKey("sk-test-value"); + assert.ok(encrypted); + assert.ok(isEncryptedApiKey(encrypted)); + assert.notEqual(encrypted, "sk-test-value"); + assert.equal(decryptApiKey(encrypted), "sk-test-value"); + assert.equal(hasStoredApiKey(encrypted), true); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("requires the encryption secret for new stored keys", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + try { + assert.throws(() => encryptApiKey("sk-test-value"), { + message: /USER_API_KEYS_ENCRYPTION_KEY/, + }); + assert.equal(decryptApiKey("legacy-plaintext"), "legacy-plaintext"); + } finally { + if (previous !== undefined) { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); +}); + +describe("MCP credential JSON blob encryption", () => { + it("encrypts and decrypts a JSON object roundtrip", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + const value = { Authorization: "Bearer sk-secret-token" }; + const encrypted = encryptJsonBlob(value); + assert.ok(encrypted, "expected ciphertext to be returned"); + assert.ok(isEncryptedApiKey(encrypted)); + assert.notEqual(encrypted, JSON.stringify(value)); + assert.deepEqual(decryptJsonBlob(encrypted), value); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("encrypts oauth-token-shaped blobs without leaking shape", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + const tokens = { + access_token: "at-123", + refresh_token: "rt-456", + token_type: "Bearer", + expires_in: 3600, + }; + const encrypted = encryptJsonBlob(tokens); + assert.ok(encrypted); + assert.equal(encrypted.includes("refresh_token"), false); + assert.equal(encrypted.includes("at-123"), false); + assert.deepEqual(decryptJsonBlob(encrypted), tokens); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("treats null and undefined as null on encrypt and decrypt", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + assert.equal(encryptJsonBlob(null), null); + assert.equal(encryptJsonBlob(undefined), null); + assert.equal(decryptJsonBlob(null), null); + assert.equal(decryptJsonBlob(undefined), null); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("passes legacy plaintext jsonb objects through unchanged on decrypt", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + const legacy = { Authorization: "Bearer legacy" }; + // Simulates a row written before encryption-at-rest landed: the + // jsonb column still holds the structured object directly. + assert.deepEqual(decryptJsonBlob(legacy), legacy); + assert.deepEqual(decryptJsonBlob([1, 2, 3]), [1, 2, 3]); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("flags legacy values as needing upgrade and skips null + already-encrypted", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + process.env.USER_API_KEYS_ENCRYPTION_KEY = "test-encryption-secret"; + try { + assert.equal(needsJsonBlobUpgrade(null), false); + assert.equal(needsJsonBlobUpgrade(undefined), false); + assert.equal( + needsJsonBlobUpgrade({ Authorization: "Bearer x" }), + true, + ); + const encrypted = encryptJsonBlob({ a: 1 })!; + assert.equal(needsJsonBlobUpgrade(encrypted), false); + } finally { + if (previous === undefined) { + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + } else { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); + + it("propagates the missing-secret throw on encrypt-time", () => { + const previous = process.env.USER_API_KEYS_ENCRYPTION_KEY; + delete process.env.USER_API_KEYS_ENCRYPTION_KEY; + try { + assert.throws( + () => encryptJsonBlob({ Authorization: "Bearer x" }), + { message: /USER_API_KEYS_ENCRYPTION_KEY/ }, + ); + } finally { + if (previous !== undefined) { + process.env.USER_API_KEYS_ENCRYPTION_KEY = previous; + } + } + }); +}); diff --git a/backend/test/downloadTokens.test.ts b/backend/test/downloadTokens.test.ts new file mode 100644 index 00000000..ebf76238 --- /dev/null +++ b/backend/test/downloadTokens.test.ts @@ -0,0 +1,38 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { signDownload, verifyDownload } from "../src/lib/downloadTokens"; + +describe("download tokens", () => { + it("requires a dedicated signing secret", () => { + const previous = process.env.DOWNLOAD_SIGNING_SECRET; + delete process.env.DOWNLOAD_SIGNING_SECRET; + try { + assert.throws(() => signDownload("docs/a.pdf", "a.pdf"), { + message: /DOWNLOAD_SIGNING_SECRET/, + }); + } finally { + if (previous !== undefined) { + process.env.DOWNLOAD_SIGNING_SECRET = previous; + } + } + }); + + it("verifies valid tokens and rejects tampering", () => { + const previous = process.env.DOWNLOAD_SIGNING_SECRET; + process.env.DOWNLOAD_SIGNING_SECRET = "test-download-secret"; + try { + const token = signDownload("docs/a.pdf", "a.pdf"); + assert.deepEqual(verifyDownload(token), { + path: "docs/a.pdf", + filename: "a.pdf", + }); + assert.equal(verifyDownload(`${token}x`), null); + } finally { + if (previous === undefined) { + delete process.env.DOWNLOAD_SIGNING_SECRET; + } else { + process.env.DOWNLOAD_SIGNING_SECRET = previous; + } + } + }); +}); diff --git a/backend/test/upload.test.ts b/backend/test/upload.test.ts new file mode 100644 index 00000000..73d6dfd5 --- /dev/null +++ b/backend/test/upload.test.ts @@ -0,0 +1,87 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import JSZip from "jszip"; +import { validateDocumentUpload } from "../src/lib/upload"; + +function upload(name: string, buffer: Buffer): Express.Multer.File { + return { + fieldname: "file", + originalname: name, + encoding: "7bit", + mimetype: "application/octet-stream", + size: buffer.length, + buffer, + stream: null as never, + destination: "", + filename: name, + path: "", + }; +} + +async function minimalDocx(): Promise { + const zip = new JSZip(); + zip.file("[Content_Types].xml", ""); + zip.file("word/document.xml", ""); + return zip.generateAsync({ type: "nodebuffer" }); +} + +describe("document upload validation", () => { + it("accepts files whose bytes match their extension", async () => { + assert.deepEqual( + await validateDocumentUpload(upload("contract.pdf", Buffer.from("%PDF-1.7"))), + { suffix: "pdf", contentType: "application/pdf" }, + ); + assert.deepEqual( + await validateDocumentUpload( + upload( + "contract.doc", + Buffer.from([ + 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, + ]), + ), + ), + { suffix: "doc", contentType: "application/msword" }, + ); + assert.deepEqual( + await validateDocumentUpload(upload("contract.docx", await minimalDocx())), + { + suffix: "docx", + contentType: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }, + ); + }); + + it("accepts a PDF with leading whitespace before %PDF-", async () => { + const buf = Buffer.concat([Buffer.from("\n"), Buffer.from("%PDF-1.7")]); + assert.deepEqual( + await validateDocumentUpload(upload("contract.pdf", buf)), + { suffix: "pdf", contentType: "application/pdf" }, + ); + }); + + it("rejects a zip renamed to .docx that lacks word/document.xml", async () => { + const zip = new JSZip(); + zip.file("random.txt", "not a word document"); + const buf = await zip.generateAsync({ type: "nodebuffer" }); + await assert.rejects( + validateDocumentUpload(upload("contract.docx", buf)), + /missing required Word document parts/, + ); + }); + + it("rejects mismatched and malformed document bytes", async () => { + await assert.rejects( + validateDocumentUpload(upload("contract.pdf", Buffer.from("not pdf"))), + /valid PDF header/, + ); + await assert.rejects( + validateDocumentUpload(upload("contract.doc", Buffer.from("not doc"))), + /legacy Word header/, + ); + await assert.rejects( + validateDocumentUpload(upload("contract.docx", Buffer.from("not zip"))), + /valid Word archive/, + ); + }); +}); diff --git a/docker-compose.yml b/docker-compose.yml index e195dfa9..d8395c7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,11 @@ services: SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + DOWNLOAD_SIGNING_SECRET: ${DOWNLOAD_SIGNING_SECRET} + USER_API_KEYS_ENCRYPTION_KEY: ${USER_API_KEYS_ENCRYPTION_KEY} + BACKEND_PUBLIC_URL: ${BACKEND_PUBLIC_URL:-http://${MIKE_HOST}:${MIKE_PORT}/backend} + MCP_ALLOW_PRIVATE_HOSTS: ${MCP_ALLOW_PRIVATE_HOSTS:-true} R2_ENDPOINT_URL: http://garage:3900 R2_BUCKET_NAME: ${R2_BUCKET_NAME} volumes: diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile index a037cbcc..5f3637d1 100644 --- a/docker/backend.Dockerfile +++ b/docker/backend.Dockerfile @@ -3,7 +3,7 @@ FROM node:22-bookworm-slim AS build WORKDIR /app COPY backend/package*.json ./ -RUN npm ci +RUN npm ci --legacy-peer-deps COPY backend/ ./ RUN npm run build diff --git a/docker/init-db.sh b/docker/init-db.sh index 9ebff94c..b7ea7043 100755 --- a/docker/init-db.sh +++ b/docker/init-db.sh @@ -27,4 +27,16 @@ done echo "init-db: applying /migrations/000_one_shot_schema.sql" psql -v ON_ERROR_STOP=1 -f /migrations/000_one_shot_schema.sql +# Apply incremental migrations in numeric order, skipping 000 (the +# one-shot schema we already applied above). Each is idempotent +# (CREATE OR REPLACE / ADD COLUMN IF NOT EXISTS / etc.) so re-running +# on every boot is safe. +ls /migrations/[0-9][0-9][0-9]_*.sql 2>/dev/null | sort | while IFS= read -r migration; do + case "$(basename "$migration")" in + 000_*) continue ;; + esac + echo "init-db: applying $migration" + psql -v ON_ERROR_STOP=1 -f "$migration" +done + echo "init-db: complete" diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 4e00a720..c0ceb714 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1,4 +1,3 @@ NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your-supabase-anon-key -SUPABASE_SECRET_KEY=your-supabase-service-role-key NEXT_PUBLIC_API_BASE_URL=http://localhost:3001 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5782999f..df55c68d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,9 +9,9 @@ "version": "0.1.0", "license": "AGPL-3.0-only", "dependencies": { - "@aws-sdk/client-s3": "^3.1025.0", - "@aws-sdk/s3-request-presigner": "^3.1025.0", - "@opennextjs/cloudflare": "^1.13.1", + "@aws-sdk/client-s3": "^3.1044.0", + "@aws-sdk/s3-request-presigner": "^3.1044.0", + "@opennextjs/cloudflare": "^1.19.8", "@openrouter/sdk": "^0.3.11", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-icons": "^1.3.2", @@ -33,11 +33,11 @@ "lucide-react": "^0.553.0", "mammoth": "^1.11.0", "marked": "^17.0.1", - "next": "16.0.3", + "next": "16.2.5", "nextjs-toploader": "^3.9.17", "pdfjs-dist": "4.10.38", - "react": "19.2.0", - "react-dom": "19.2.0", + "react": "19.2.6", + "react-dom": "19.2.6", "react-markdown": "^10.1.0", "recharts": "^3.7.0", "rehype-katex": "^7.0.1", @@ -59,7 +59,7 @@ "babel-plugin-react-compiler": "1.0.0", "baseline-browser-mapping": "^2.9.11", "eslint": "^9", - "eslint-config-next": "16.0.3", + "eslint-config-next": "16.2.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", @@ -497,6 +497,22 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-cloudfront/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.984.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.984.0.tgz", @@ -550,6 +566,22 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-lambda": { "version": "3.984.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.984.0.tgz", @@ -605,82 +637,82 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.1025.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1025.0.tgz", - "integrity": "sha512-9Byz2fPnuGRRL8DTTD5bYPl1Iwm+ysLiCMgptffa3lNkVLCiUZc5e5TAaOjk0MvyeXieq+jn35AmQL6cgN2KHQ==", + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/credential-provider-node": "^3.972.29", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", - "@aws-sdk/middleware-expect-continue": "^3.972.8", - "@aws-sdk/middleware-flexible-checksums": "^3.974.6", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-location-constraint": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-sdk-s3": "^3.972.27", - "@aws-sdk/middleware-ssec": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.28", - "@aws-sdk/region-config-resolver": "^3.972.10", - "@aws-sdk/signature-v4-multi-region": "^3.996.15", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.14", - "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.13", - "@smithy/eventstream-serde-browser": "^4.2.12", - "@smithy/eventstream-serde-config-resolver": "^4.3.12", - "@smithy/eventstream-serde-node": "^4.2.12", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-blob-browser": "^4.2.13", - "@smithy/hash-node": "^4.2.12", - "@smithy/hash-stream-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/md5-js": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-retry": "^4.4.46", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.44", - "@smithy/util-defaults-mode-node": "^4.2.48", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", - "@smithy/util-stream": "^4.5.21", - "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.14", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "node_modules/@aws-sdk/client-s3": { + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1044.0.tgz", + "integrity": "sha512-yT3g0Oi0b+pJBJswNxRwWLLBoExQhRx9Iz2rUy1xV0slMogTQN+DSjChI95XTDtpGEcY0qnIK6UYX0XCYdhOKg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", + "@aws-sdk/middleware-expect-continue": "^3.972.10", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-location-constraint": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/middleware-ssec": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-blob-browser": "^4.2.15", + "@smithy/hash-node": "^4.2.14", + "@smithy/hash-stream-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/md5-js": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -739,23 +771,40 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/core": { - "version": "3.973.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.26.tgz", - "integrity": "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==", + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.16", - "@smithy/core": "^3.23.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -764,12 +813,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", - "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz", + "integrity": "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -777,15 +826,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz", - "integrity": "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -793,20 +842,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz", - "integrity": "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -814,24 +863,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.28.tgz", - "integrity": "sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-login": "^3.972.28", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.28", - "@aws-sdk/credential-provider-web-identity": "^3.972.28", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -839,18 +888,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.28.tgz", - "integrity": "sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -858,22 +907,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.29.tgz", - "integrity": "sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.24", - "@aws-sdk/credential-provider-http": "^3.972.26", - "@aws-sdk/credential-provider-ini": "^3.972.28", - "@aws-sdk/credential-provider-process": "^3.972.24", - "@aws-sdk/credential-provider-sso": "^3.972.28", - "@aws-sdk/credential-provider-web-identity": "^3.972.28", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -881,16 +930,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz", - "integrity": "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -898,18 +947,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.28.tgz", - "integrity": "sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/token-providers": "3.1021.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -917,17 +966,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.28.tgz", - "integrity": "sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -935,15 +984,14 @@ } }, "node_modules/@aws-sdk/dynamodb-codec": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.27.tgz", - "integrity": "sha512-S7IWE0K+aqbvjP8PHnOyDJK1fzrazAismH5XutJtS3YBvRvmfLb8Ac7Z1ZC4LBWvO8Gx1t/szFe46K51FqZn/A==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.8.tgz", + "integrity": "sha512-dYQ/cQqHZd23hcl8oEGwPphTqyGnmvf2HrVmz4J90Q5Bv89oJjlwcBcifiiTvApqsVpx7Pr0IebMpkYwWJvZlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@smithy/core": "^3.23.13", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@smithy/core": "^3.23.17", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -965,16 +1013,16 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", - "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz", + "integrity": "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -983,16 +1031,16 @@ } }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.9.tgz", - "integrity": "sha512-1503Y5Xk14SdXY0ucXwc08CY+aVuoY1tmQxsR/apwAVAwcLT7FFzqjYJYLq8JOkKJyzIB8M6J27e1ZcagGK+Fg==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.11.tgz", + "integrity": "sha512-vXARCZVFQHdsd6qPPZyC/hh+5x2XsCYKqUQDCqnUlpGpChMpDojOOacQWdLJ+FFXKN8X3cmLOGrtgx/zysCKqQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.5", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1000,14 +1048,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", - "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz", + "integrity": "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1015,23 +1063,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.6.tgz", - "integrity": "sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ==", + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/crc64-nvme": "^3.972.5", - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/crc64-nvme": "^3.972.7", + "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.21", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1040,14 +1088,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1055,13 +1103,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", - "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", + "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1069,13 +1117,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1083,15 +1131,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", - "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1099,23 +1147,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.27.tgz", - "integrity": "sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.21", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1124,14 +1172,14 @@ } }, "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.18.tgz", - "integrity": "sha512-BdsGFuBJUX5PnuZkEV6JRB5g/6ts7iGmN3pXwyoiGCCM2HHXrlFqjkBs+iPX7yO884WqYeQJpme7nwn4DzU5xw==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.22.tgz", + "integrity": "sha512-DtR3mEiOUJcnEX/QuXmvbJto6xvQzp2ftnHb29c0aQYdmmzbKf0gsu9ovx1i/yy4ZR6m0rttTucS0iiP32dlGA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -1141,13 +1189,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", - "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", + "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1155,34 +1203,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.28.tgz", - "integrity": "sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.13", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", "tslib": "^2.6.2" }, "engines": { @@ -1190,47 +1222,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.18.tgz", - "integrity": "sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==", + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.9", - "@aws-sdk/middleware-user-agent": "^3.972.28", - "@aws-sdk/region-config-resolver": "^3.972.10", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.14", - "@smithy/config-resolver": "^4.4.13", - "@smithy/core": "^3.23.13", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-retry": "^4.4.46", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.44", - "@smithy/util-defaults-mode-node": "^4.2.48", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1238,32 +1271,16 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz", - "integrity": "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==", + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.13", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1271,18 +1288,18 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1025.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1025.0.tgz", - "integrity": "sha512-5kiXbyfUjPJIIVIvKoLNaiHk0vh93UeB5QUjJa4ZTGPr08dJh7oCzY3JKT/dNdr20uUO+qxVkhVQ4ZI9Tmhx8A==", + "version": "3.1044.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1044.0.tgz", + "integrity": "sha512-ix8UtiNC5g1wv3TIcgTnvWdugyw8dSsBGwZZzVVoGyYjZH9UJLqiOyvVu6apptlPBeE6aV6Fabsx0b1xYFd2ZA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "^3.996.15", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-format-url": "^3.972.8", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1290,16 +1307,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.15.tgz", - "integrity": "sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ==", + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.27", - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1307,17 +1324,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1021.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1021.0.tgz", - "integrity": "sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==", + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.26", - "@aws-sdk/nested-clients": "^3.996.18", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1325,12 +1342,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1350,15 +1367,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.984.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", - "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" }, "engines": { @@ -1366,14 +1383,14 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", - "integrity": "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1393,27 +1410,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.14", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.14.tgz", - "integrity": "sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.28", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -1430,13 +1447,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", - "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.8", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -3419,15 +3437,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz", - "integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.5.tgz", + "integrity": "sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.3.tgz", - "integrity": "sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.5.tgz", + "integrity": "sha512-PyILm/cw2u5gEG5xOjqFbALUAl/erAqtM47iZtP9lXiSzin+eOIf3KRi+CBC/mFG9j7Iz3JDqCOY94nFLUCccg==", "dev": true, "license": "MIT", "dependencies": { @@ -3435,9 +3453,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz", - "integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.5.tgz", + "integrity": "sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==", "cpu": [ "arm64" ], @@ -3451,9 +3469,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz", - "integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.5.tgz", + "integrity": "sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==", "cpu": [ "x64" ], @@ -3467,9 +3485,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz", - "integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.5.tgz", + "integrity": "sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==", "cpu": [ "arm64" ], @@ -3483,9 +3501,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz", - "integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.5.tgz", + "integrity": "sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==", "cpu": [ "arm64" ], @@ -3499,9 +3517,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz", - "integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.5.tgz", + "integrity": "sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==", "cpu": [ "x64" ], @@ -3515,9 +3533,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz", - "integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.5.tgz", + "integrity": "sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==", "cpu": [ "x64" ], @@ -3531,9 +3549,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz", - "integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.5.tgz", + "integrity": "sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==", "cpu": [ "arm64" ], @@ -3547,9 +3565,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz", - "integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.5.tgz", + "integrity": "sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==", "cpu": [ "x64" ], @@ -3601,6 +3619,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@node-minify/core": { "version": "8.0.6", "resolved": "https://registry.npmjs.org/@node-minify/core/-/core-8.0.6.tgz", @@ -3616,9 +3646,9 @@ } }, "node_modules/@node-minify/core/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3772,9 +3802,9 @@ } }, "node_modules/@opennextjs/aws": { - "version": "3.9.16", - "resolved": "https://registry.npmjs.org/@opennextjs/aws/-/aws-3.9.16.tgz", - "integrity": "sha512-jQQStCysIllNCPqz5W2KSguXpr+ETlOcD8SyNu+h9zwpRVYk4uEPQge+ErG3avI5xsT8vKA7EGLYG59dhj/B6Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@opennextjs/aws/-/aws-4.0.1.tgz", + "integrity": "sha512-k+wV8xyl2koaQRp84EY++3tO1J/M0b2KK4zR0LrPSwDgPqcR9EaKYiUu1mugc79A0KVgo839KR+opgk3wpSsXw==", "license": "MIT", "dependencies": { "@ast-grep/napi": "^0.40.5", @@ -3799,7 +3829,7 @@ "open-next": "dist/index.js" }, "peerDependencies": { - "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5" + "next": ">=15.5.16 <16 || >=16.2.5" } }, "node_modules/@opennextjs/aws/node_modules/@aws-sdk/client-s3": { @@ -3885,15 +3915,32 @@ "node": ">=20.0.0" } }, + "node_modules/@opennextjs/aws/node_modules/@aws-sdk/util-endpoints": { + "version": "3.984.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.984.0.tgz", + "integrity": "sha512-9ebjLA0hMKHeVvXEtTDCCOBtwjb0bOXiuUV06HNeVdgAjH6gj4x4Zwt4IBti83TiyTGOCl5YfZqGx4ehVsasbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@opennextjs/cloudflare": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@opennextjs/cloudflare/-/cloudflare-1.18.0.tgz", - "integrity": "sha512-JM236YHnKzroFAZqst1t28ZGOShvnkVUDtjrp7TJ/W2P3RLo4b6npJ8VEXOn6frs6lsUfR5rvsKYLYb7h1GIJQ==", + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@opennextjs/cloudflare/-/cloudflare-1.19.8.tgz", + "integrity": "sha512-4c8gFgVWsuH+g42b1/tmltWeeGrM+vK+yx3v7sQS4ZdnjB5Oh4KHjOBuSyv/ZOKjad+67RxLZT1uP7yDKK/Y6w==", "license": "MIT", "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", - "@opennextjs/aws": "3.9.16", + "@opennextjs/aws": "4.0.1", + "ci-info": "^4.2.0", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", @@ -3905,8 +3952,8 @@ "opennextjs-cloudflare": "dist/cli/index.js" }, "peerDependencies": { - "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5", - "wrangler": "^4.65.0" + "next": ">=15.5.16 <16 || >=16.2.5", + "wrangler": "^4.86.0" } }, "node_modules/@openrouter/sdk": { @@ -4632,16 +4679,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", - "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -4649,18 +4696,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.13", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.13.tgz", - "integrity": "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==", + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.21", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -4670,15 +4717,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -4686,13 +4733,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", - "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -4701,13 +4748,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", - "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4715,12 +4762,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", - "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4728,13 +4775,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", - "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4742,13 +4789,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", - "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4756,14 +4803,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -4772,14 +4819,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", - "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4787,12 +4834,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -4802,12 +4849,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", - "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -4816,12 +4863,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4841,12 +4888,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", - "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -4855,13 +4902,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4869,18 +4916,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.28", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz", - "integrity": "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==", + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-serde": "^4.2.16", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -4888,18 +4935,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.46", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.46.tgz", - "integrity": "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.13", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -4908,14 +4956,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz", - "integrity": "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4923,12 +4971,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4936,14 +4984,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4951,14 +4999,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz", - "integrity": "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4966,12 +5014,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4979,12 +5027,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -4992,12 +5040,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -5006,12 +5054,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5019,24 +5067,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1" + "@smithy/types": "^4.14.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5044,16 +5092,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -5063,17 +5111,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.8.tgz", - "integrity": "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==", + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.13", - "@smithy/middleware-endpoint": "^4.4.28", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.21", + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" }, "engines": { @@ -5081,9 +5129,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5093,13 +5141,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5170,14 +5218,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.44", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.44.tgz", - "integrity": "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==", + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5185,17 +5233,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.48", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz", - "integrity": "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==", + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.13", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.8", - "@smithy/types": "^4.13.1", + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5203,13 +5251,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5229,12 +5277,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5242,13 +5290,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.13.tgz", - "integrity": "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -5256,14 +5304,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.21", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.21.tgz", - "integrity": "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==", + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.1", - "@smithy/types": "^4.13.1", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -5300,12 +5348,12 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.14.tgz", - "integrity": "sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -7070,9 +7118,9 @@ ] }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -7573,7 +7621,6 @@ "version": "2.10.14", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -7962,6 +8009,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -8160,9 +8222,9 @@ "license": "MIT" }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -9181,13 +9243,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.3.tgz", - "integrity": "sha512-5F6qDjcZldf0Y0ZbqvWvap9xzYUxyDf7/of37aeyhvkrQokj/4bT1JYWZdlWUr283aeVa+s52mPq9ogmGg+5dw==", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.5.tgz", + "integrity": "sha512-fXEkugikngux1FBJ/Vop+52SLAMFjXZFXjyl/+HjGHngnXf8iIfqe3qdjcwN+40RBpSsCVhI04j0/ngEWL5Qng==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.3", + "@next/eslint-plugin-next": "16.2.5", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -9823,9 +9885,9 @@ "license": "Unlicense" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz", + "integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==", "funding": [ { "type": "github", @@ -9838,9 +9900,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -9849,9 +9911,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -13522,14 +13585,14 @@ } }, "node_modules/next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz", - "integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "16.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz", + "integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==", "license": "MIT", "dependencies": { - "@next/env": "16.0.3", + "@next/env": "16.2.5", "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -13541,15 +13604,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.3", - "@next/swc-darwin-x64": "16.0.3", - "@next/swc-linux-arm64-gnu": "16.0.3", - "@next/swc-linux-arm64-musl": "16.0.3", - "@next/swc-linux-x64-gnu": "16.0.3", - "@next/swc-linux-x64-musl": "16.0.3", - "@next/swc-win32-arm64-msvc": "16.0.3", - "@next/swc-win32-x64-msvc": "16.0.3", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.5", + "@next/swc-darwin-x64": "16.2.5", + "@next/swc-linux-arm64-gnu": "16.2.5", + "@next/swc-linux-arm64-musl": "16.2.5", + "@next/swc-linux-x64-gnu": "16.2.5", + "@next/swc-linux-x64-musl": "16.2.5", + "@next/swc-win32-arm64-msvc": "16.2.5", + "@next/swc-win32-x64-msvc": "16.2.5", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -13574,52 +13637,6 @@ } } }, - "node_modules/next/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/nextjs-toploader": { "version": "3.9.17", "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", @@ -14083,9 +14100,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz", - "integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -14207,10 +14224,9 @@ "license": "MIT-0" }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -14239,7 +14255,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -14519,9 +14534,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14579,24 +14594,24 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.6" } }, "node_modules/react-is": { @@ -16066,9 +16081,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -17814,9 +17829,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/frontend/package.json b/frontend/package.json index 520d74dc..aa76d576 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,9 @@ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts" }, "dependencies": { - "@aws-sdk/client-s3": "^3.1025.0", - "@aws-sdk/s3-request-presigner": "^3.1025.0", - "@opennextjs/cloudflare": "^1.13.1", + "@aws-sdk/client-s3": "^3.1044.0", + "@aws-sdk/s3-request-presigner": "^3.1044.0", + "@opennextjs/cloudflare": "^1.19.8", "@openrouter/sdk": "^0.3.11", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-icons": "^1.3.2", @@ -38,11 +38,11 @@ "lucide-react": "^0.553.0", "mammoth": "^1.11.0", "marked": "^17.0.1", - "next": "16.0.3", + "next": "16.2.5", "nextjs-toploader": "^3.9.17", "pdfjs-dist": "4.10.38", - "react": "19.2.0", - "react-dom": "19.2.0", + "react": "19.2.6", + "react-dom": "19.2.6", "react-markdown": "^10.1.0", "recharts": "^3.7.0", "rehype-katex": "^7.0.1", @@ -54,6 +54,10 @@ "tailwind-merge": "^3.4.0", "tiptap-markdown": "^0.9.0" }, + "overrides": { + "@xmldom/xmldom": "0.8.13", + "postcss": "8.5.10" + }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/marked": "^5.0.2", @@ -64,7 +68,7 @@ "babel-plugin-react-compiler": "1.0.0", "baseline-browser-mapping": "^2.9.11", "eslint": "^9", - "eslint-config-next": "16.0.3", + "eslint-config-next": "16.2.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5", diff --git a/frontend/src/app/(pages)/account/layout.tsx b/frontend/src/app/(pages)/account/layout.tsx index 543638c1..475fa81d 100644 --- a/frontend/src/app/(pages)/account/layout.tsx +++ b/frontend/src/app/(pages)/account/layout.tsx @@ -14,6 +14,7 @@ interface TabDef { const TABS: TabDef[] = [ { id: "general", label: "General", href: "/account" }, { id: "models", label: "Models & API Keys", href: "/account/models" }, + { id: "mcp", label: "Connectors", href: "/account/mcp" }, ]; export default function AccountLayout({ diff --git a/frontend/src/app/(pages)/account/mcp/page.tsx b/frontend/src/app/(pages)/account/mcp/page.tsx new file mode 100644 index 00000000..13d3ef60 --- /dev/null +++ b/frontend/src/app/(pages)/account/mcp/page.tsx @@ -0,0 +1,761 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { + AlertCircle, + Check, + ChevronUp, + Loader2, + Plus, + Trash2, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + createMcpServer, + deleteMcpServer, + listMcpServers, + startMcpOauth, + testMcpServer, + updateMcpServer, + type McpServer, + type McpServerTestResult, +} from "@/app/lib/mikeApi"; + +type DraftHeader = { key: string; value: string }; + +type Draft = { + name: string; + url: string; + headers: DraftHeader[]; + auth_type: "headers" | "oauth"; +}; + +const EMPTY_DRAFT: Draft = { + name: "", + url: "", + headers: [{ key: "", value: "" }], + auth_type: "headers", +}; + +export default function McpServersPage() { + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + + const [showAdd, setShowAdd] = useState(false); + const [draft, setDraft] = useState(EMPTY_DRAFT); + const [saving, setSaving] = useState(false); + const [addError, setAddError] = useState(null); + + const [testing, setTesting] = useState>({}); + const [testResults, setTestResults] = useState< + Record + >({}); + + const reload = useCallback(async () => { + setLoadError(null); + try { + const list = await listMcpServers(); + setServers(list); + } catch (err) { + setLoadError(err instanceof Error ? err.message : "Failed to load"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + reload(); + }, [reload]); + + const handleAdd = async () => { + setAddError(null); + const name = draft.name.trim(); + const url = draft.url.trim(); + if (!name || !url) { + setAddError("Name and URL are required."); + return; + } + const headers: Record = {}; + for (const h of draft.headers) { + const k = h.key.trim(); + if (!k) continue; + headers[k] = h.value; + } + setSaving(true); + try { + const created = await createMcpServer({ + name, + url, + headers: draft.auth_type === "oauth" ? {} : headers, + auth_type: draft.auth_type, + }); + setDraft(EMPTY_DRAFT); + setShowAdd(false); + await reload(); + if (draft.auth_type === "oauth") { + // Discovery + sign-in needs user interaction. Kick the popup + // immediately so it feels like one continuous flow. + void launchOAuth(created.id); + } else { + // Auto-discover tools so the user sees the tool list right + // away without an extra Test click. + void runAutoTest(created.id); + } + } catch (err) { + setAddError(err instanceof Error ? err.message : "Failed to save"); + } finally { + setSaving(false); + } + }; + + const launchOAuth = async (id: string) => { + try { + const { authorize_url, already_authorized } = await startMcpOauth(id); + if (already_authorized) { + await reload(); + void runAutoTest(id); + return; + } + if (!authorize_url) { + alert("Authorization server did not return a URL."); + return; + } + const popup = window.open( + authorize_url, + "mcp_oauth", + "width=600,height=720,menubar=no,toolbar=no,location=no", + ); + if (!popup) { + alert( + "Couldn't open the sign-in window — check your popup blocker.", + ); + return; + } + // Poll the row until tokens are saved, or until the popup closes + // unfinished. Stop after 5 minutes regardless. + const deadline = Date.now() + 5 * 60 * 1000; + const interval = setInterval(async () => { + try { + const list = await listMcpServers(); + const row = list.find((s) => s.id === id); + if (row?.oauth_authorized) { + clearInterval(interval); + try { + popup.close(); + } catch { + /* ignore */ + } + setServers(list); + void runAutoTest(id); + return; + } + } catch { + /* ignore transient errors */ + } + if (popup.closed || Date.now() > deadline) { + clearInterval(interval); + await reload(); + } + }, 1500); + } catch (err) { + alert(err instanceof Error ? err.message : "Sign-in failed"); + } + }; + + const runAutoTest = async (id: string) => { + setTesting((s) => ({ ...s, [id]: true })); + try { + const result = await testMcpServer(id); + setTestResults((r) => ({ ...r, [id]: result })); + } catch (err) { + setTestResults((r) => ({ + ...r, + [id]: { + ok: false, + error: err instanceof Error ? err.message : String(err), + }, + })); + } finally { + setTesting((s) => ({ ...s, [id]: false })); + reload(); + } + }; + + const handleToggleEnabled = async (server: McpServer) => { + const wasDisabled = !server.enabled; + try { + await updateMcpServer(server.id, { enabled: !server.enabled }); + await reload(); + // Auto-test when going disabled → enabled so the user sees + // immediately if the server still works after re-enabling. + if (wasDisabled) void runAutoTest(server.id); + } catch (err) { + alert(err instanceof Error ? err.message : "Failed to update"); + } + }; + + const handleDelete = async (server: McpServer) => { + if (!confirm(`Remove connector "${server.name}"?`)) return; + try { + await deleteMcpServer(server.id); + await reload(); + } catch (err) { + alert(err instanceof Error ? err.message : "Failed to delete"); + } + }; + + const handleTest = async (server: McpServer) => { + setTesting((s) => ({ ...s, [server.id]: true })); + try { + const result = await testMcpServer(server.id); + setTestResults((r) => ({ ...r, [server.id]: result })); + } catch (err) { + setTestResults((r) => ({ + ...r, + [server.id]: { + ok: false, + error: err instanceof Error ? err.message : String(err), + }, + })); + } finally { + setTesting((s) => ({ ...s, [server.id]: false })); + // Reload so last_error reflects the test outcome. + reload(); + } + }; + + return ( +
+
+
+

+ Connectors +

+ +
+

+ Connectors plug external tools into Mike via the{" "} + + Model Context Protocol + {" "} + (MCP) — legal-data sources, web research, internal + company APIs, and so on. Tools discovered from each + connector become available to the chat assistant under + the{" "} + + mcp__<slug>__<tool> + {" "} + name. +

+
+ + {/* Trust trade-off warning. Surfaced once at the top so users + don't paste URLs and tokens for servers they haven't vetted. */} +
+ +
+

+ Only add connectors you trust +

+

+ A connector’s operator can see anything Mike + sends in tool calls — your prompts, document + excerpts, and the tool’s own response. Custom + headers (including{" "} + + Authorization + {" "} + tokens) are sent on every request. +

+
+
+ + {showAdd && ( + + )} + + {loading ? ( +
+ Loading + servers… +
+ ) : loadError ? ( +
+ + {loadError} +
+ ) : servers.length === 0 ? ( +
+ No connectors configured yet. +
+ ) : ( +
+ {servers.map((s) => ( + handleToggleEnabled(s)} + onDelete={() => handleDelete(s)} + onTest={() => handleTest(s)} + onSignIn={() => launchOAuth(s.id)} + /> + ))} +
+ )} +
+ ); +} + +function AddForm({ + draft, + setDraft, + onSave, + saving, + error, +}: { + draft: Draft; + setDraft: (d: Draft) => void; + onSave: () => void; + saving: boolean; + error: string | null; +}) { + const updateHeader = (idx: number, patch: Partial) => { + const headers = draft.headers.map((h, i) => + i === idx ? { ...h, ...patch } : h, + ); + setDraft({ ...draft, headers }); + }; + const addHeaderRow = () => + setDraft({ + ...draft, + headers: [...draft.headers, { key: "", value: "" }], + }); + const removeHeaderRow = (idx: number) => + setDraft({ + ...draft, + headers: draft.headers.filter((_, i) => i !== idx), + }); + + return ( +
+
+ + + setDraft({ ...draft, name: e.target.value }) + } + /> +
+
+ + + setDraft({ ...draft, url: e.target.value }) + } + /> +
+
+ +
+ + +
+
+ {draft.auth_type === "headers" && ( +
+ +

+ Sent on every request. Common usage:{" "} + + Authorization + {" "} + →{" "} + + Bearer <token> + + . +

+
+ {draft.headers.map((h, idx) => ( +
+ + updateHeader(idx, { key: e.target.value }) + } + className="flex-1" + /> + + updateHeader(idx, { + value: e.target.value, + }) + } + className="flex-1" + type="password" + /> + +
+ ))} + +
+
+ )} + {error && ( +
+ + {error} +
+ )} +
+ +
+
+ ); +} + +/** + * Sanitize a user-supplied server name for safe rendering. Strips Bearer + * prefixes and obvious secret-looking tokens that users sometimes paste into + * the Name field by mistake — the chat surface uses this label, so we don't + * want secrets leaking onto screens / screenshots. + */ +function safeServerName(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) return "Untitled connector"; + const looksLikeSecret = + /\b(?:Bearer|Basic|sk-[A-Za-z0-9_-]{8,}|sb_secret_|AIza[A-Za-z0-9_-]{20,})\b/i.test( + trimmed, + ); + if (looksLikeSecret) { + // Best-effort cleanup: strip the secret-shaped substring. + const cleaned = trimmed + .replace( + /\s*\(?(Bearer|Basic)\s+[A-Za-z0-9._~+/\-]+=*\)?/gi, + "", + ) + .replace(/sk-[A-Za-z0-9_-]{8,}/g, "") + .replace(/sb_secret_[A-Za-z0-9_-]+/g, "") + .replace(/AIza[A-Za-z0-9_-]{20,}/g, "") + .replace(/\s{2,}/g, " ") + .trim(); + return cleaned || "Untitled connector"; + } + return trimmed; +} + +function ServerCard({ + server, + testing, + testResult, + onToggle, + onDelete, + onTest, + onSignIn, +}: { + server: McpServer; + testing: boolean; + testResult?: McpServerTestResult; + onToggle: () => void; + onDelete: () => void; + onTest: () => void; + onSignIn: () => void; +}) { + const [showDetails, setShowDetails] = useState(false); + const displayName = safeServerName(server.name); + const nameWasSanitized = displayName !== server.name.trim(); + const needsSignIn = + server.auth_type === "oauth" && !server.oauth_authorized; + + return ( +
+ {/* Header */} +
+
+
+

+ {displayName} +

+ {server.auth_type === "oauth" && ( + + {server.oauth_authorized + ? "OAuth · signed in" + : "OAuth · sign-in required"} + + )} + {server.enabled ? ( + + + Enabled + + ) : ( + + + Disabled + + )} + {server.last_error && server.last_error !== "reauth_required" && ( + + + Error + + )} +
+ {nameWasSanitized && ( +

+ Name contained what looks like a secret — + displayed redacted. Edit the server to fix. +

+ )} + + {server.url} + + {server.header_keys.length > 0 && ( +
+ Headers: + {server.header_keys.map((k) => ( + + {k} + + ))} +
+ )} +
+
+ {needsSignIn ? ( + + ) : ( + + )} + + +
+
+ + {/* Errors / status footer */} + {testResult && !testResult.ok && ( +
+ + + {testResult.error ?? "Unknown error"} + +
+ )} + {server.last_error && !testResult && ( +
+ + {server.last_error} +
+ )} + + {/* Tool list */} + {testResult?.ok && testResult.tools && testResult.tools.length > 0 && ( +
+ + {showDetails && ( +
    + {testResult.tools.map((t) => ( + + ))} +
+ )} +
+ )} +
+ ); +} + +function ToolListItem({ + name, + description, +}: { + name: string; + description: string; +}) { + const [expanded, setExpanded] = useState(false); + const trimmed = description.trim(); + const isLong = trimmed.length > 160; + const shown = expanded || !isLong ? trimmed : trimmed.slice(0, 160) + "…"; + return ( +
  • +
    + + {name} + + {isLong && ( + + )} +
    + {trimmed && ( +

    {shown}

    + )} +
  • + ); +} + diff --git a/frontend/src/app/(pages)/account/models/page.tsx b/frontend/src/app/(pages)/account/models/page.tsx index cf3720ea..df4eb07d 100644 --- a/frontend/src/app/(pages)/account/models/page.tsx +++ b/frontend/src/app/(pages)/account/models/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { AlertCircle, Check, ChevronDown, Eye, EyeOff } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -15,6 +15,7 @@ import { import { useUserProfile } from "@/contexts/UserProfileContext"; import { MODELS } from "@/app/components/assistant/ModelToggle"; import { + apiKeysFromProfile, isModelAvailable, modelGroupToProvider, } from "@/app/lib/modelAvailability"; @@ -41,10 +42,7 @@ export default function ModelsAndApiKeysPage() { profile?.tabularModel ?? "gemini-3-flash-preview" } - apiKeys={{ - claudeApiKey: profile?.claudeApiKey ?? null, - geminiApiKey: profile?.geminiApiKey ?? null, - }} + apiKeys={apiKeysFromProfile(profile)} onChange={(id) => updateModelPreference("tabularModel", id) } @@ -74,17 +72,25 @@ export default function ModelsAndApiKeysPage() { - updateApiKey("claude", value.trim() || null) + updateApiKey("claude", value?.trim() || null) } /> - updateApiKey("gemini", value.trim() || null) + updateApiKey("gemini", value?.trim() || null) + } + /> + + updateApiKey("openrouter", value?.trim() || null) } /> @@ -100,12 +106,12 @@ function TabularModelDropdown({ }: { value: string; onChange: (id: string) => void; - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }; + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openrouterApiKey: string | null }; }) { const [isOpen, setIsOpen] = useState(false); const selected = MODELS.find((m) => m.id === value); const selectedAvailable = isModelAvailable(value, apiKeys); - const groups: ("Anthropic" | "Google")[] = ["Anthropic", "Google"]; + const groups: ("Anthropic" | "Google" | "OpenRouter")[] = ["Anthropic", "Google", "OpenRouter"]; return ( @@ -154,7 +160,7 @@ function TabularModelDropdown({ onSelect={() => onChange(m.id)} title={ !available - ? `Add a ${provider === "claude" ? "Claude" : "Gemini"} API key to use this model` + ? `Add a ${provider === "claude" ? "Claude" : provider === "gemini" ? "Gemini" : "OpenRouter"} API key to use this model` : undefined } > @@ -183,30 +189,27 @@ function TabularModelDropdown({ function ApiKeyField({ label, placeholder, - initialValue, + hasKey, onSave, }: { label: string; placeholder: string; - initialValue: string; - onSave: (value: string) => Promise; + hasKey: boolean; + onSave: (value: string | null) => Promise; }) { - const [value, setValue] = useState(initialValue); + const [value, setValue] = useState(""); const [reveal, setReveal] = useState(false); const [isSaving, setIsSaving] = useState(false); const [saved, setSaved] = useState(false); - useEffect(() => { - setValue(initialValue); - }, [initialValue]); - - const dirty = value !== initialValue; + const dirty = value.trim().length > 0; const handleSave = async () => { setIsSaving(true); const ok = await onSave(value); setIsSaving(false); if (ok) { + setValue(""); setSaved(true); setTimeout(() => setSaved(false), 2000); } else { @@ -214,6 +217,17 @@ function ApiKeyField({ } }; + const handleClear = async () => { + setIsSaving(true); + const ok = await onSave(null); + setIsSaving(false); + if (!ok) { + alert(`Failed to clear ${label}.`); + } else { + setValue(""); + } + }; + return (
    @@ -223,7 +237,11 @@ function ApiKeyField({ type={reveal ? "text" : "password"} value={value} onChange={(e) => setValue(e.target.value)} - placeholder={placeholder} + placeholder={ + hasKey + ? "Configured - enter a new key to replace" + : placeholder + } className="pr-10" autoComplete="off" spellCheck={false} @@ -257,6 +275,16 @@ function ApiKeyField({ "Save" )} + {hasKey && !dirty && ( + + )}
    ); diff --git a/frontend/src/app/components/assistant/AssistantMessage.tsx b/frontend/src/app/components/assistant/AssistantMessage.tsx index 48b0425b..46c17d1c 100644 --- a/frontend/src/app/components/assistant/AssistantMessage.tsx +++ b/frontend/src/app/components/assistant/AssistantMessage.tsx @@ -315,14 +315,25 @@ function ResponseStatus({ status }: { status: StatusState }) { const isError = status === "error"; useEffect(() => { + let hideTimer: ReturnType | null = null; if (wasActiveRef.current && !isActive) { - setShowDone(true); - setDoneVisible(true); - const t = setTimeout(() => setDoneVisible(false), 1500); - return () => clearTimeout(t); + const showTimer = setTimeout(() => { + setShowDone(true); + setDoneVisible(true); + hideTimer = setTimeout(() => setDoneVisible(false), 1500); + }, 0); + wasActiveRef.current = isActive; + return () => { + clearTimeout(showTimer); + if (hideTimer) clearTimeout(hideTimer); + }; } else if (!wasActiveRef.current && isActive) { - setShowDone(false); - setDoneVisible(false); + const resetTimer = setTimeout(() => { + setShowDone(false); + setDoneVisible(false); + }, 0); + wasActiveRef.current = isActive; + return () => clearTimeout(resetTimer); } wasActiveRef.current = isActive; }, [isActive]); @@ -421,6 +432,89 @@ function ReasoningBlock({ ); } +function McpToolResultBlock({ + server, + tool, + ok, + args, + output, + showConnector, +}: { + server: string; + tool: string; + ok: boolean; + args: string; + output: string; + showConnector?: boolean; +}) { + const [expanded, setExpanded] = useState(false); + const prettyArgs = (() => { + try { + const parsed = JSON.parse(args); + return JSON.stringify(parsed, null, 2); + } catch { + return args; + } + })(); + const outputPreview = output.split("\n").slice(0, 1).join("\n"); + const outputClamped = + outputPreview.length > 160 + ? outputPreview.slice(0, 160) + "…" + : outputPreview; + return ( +
    + {showConnector && ( +
    + )} +
    +
    + +
    + {expanded && ( +
    +
    +
    + Arguments +
    +
    +                            {prettyArgs || "(none)"}
    +                        
    +
    +
    +
    + Output +
    +
    +                            {output || "(empty)"}
    +                        
    +
    +
    + )} +
    + ); +} + function DocReadBlock({ filename, onClick, @@ -893,7 +987,9 @@ function MarkdownContent({ /> ), p: ({ node, ...props }) => { - const parent = (node as any)?.parent; + const parent = ( + node as { parent?: { type?: string } } | undefined + )?.parent; if (parent?.type === "listItem") { return (

    { - console.log( - "[AssistantMessage] citation clicked", - annotation, - ); onCitationClick?.(annotation); }} className="mx-0.5 inline-flex items-center justify-center rounded-full w-4 h-4 text-[10px] font-medium transition-colors align-super bg-gray-100 text-gray-900 hover:bg-gray-200" @@ -1089,7 +1181,6 @@ export function AssistantMessage({ versionId: string | null; downloadUrl: string | null; }) => { - console.log("[AssistantMessage] handleEditResolved", args); if (args.downloadUrl) { setResolvedOverrides((prev) => ({ ...prev, @@ -1239,7 +1330,11 @@ export function AssistantMessage({

    Running - {event.name ? `${event.name}...` : "tool..."} + {event.display_name + ? `${event.display_name}...` + : event.name + ? `${event.name}...` + : "tool..."}
    ); @@ -1336,6 +1431,19 @@ export function AssistantMessage({ /> ); } + if (event.type === "mcp_tool_result") { + return ( + + ); + } return null; }; diff --git a/frontend/src/app/components/assistant/ChatInput.tsx b/frontend/src/app/components/assistant/ChatInput.tsx index 7f56192b..5ca2722a 100644 --- a/frontend/src/app/components/assistant/ChatInput.tsx +++ b/frontend/src/app/components/assistant/ChatInput.tsx @@ -21,10 +21,12 @@ import { AddDocButton } from "./AddDocButton"; import { AddDocumentsModal } from "../shared/AddDocumentsModal"; import { AssistantWorkflowModal } from "./AssistantWorkflowModal"; import { ApiKeyMissingModal } from "../shared/ApiKeyMissingModal"; +import { McpToggleButton } from "./McpToggleButton"; import { ModelToggle } from "./ModelToggle"; import { useSelectedModel } from "@/app/hooks/useSelectedModel"; import { useUserProfile } from "@/contexts/UserProfileContext"; import { + apiKeysFromProfile, getModelProvider, isModelAvailable, type ModelProvider, @@ -67,10 +69,7 @@ export const ChatInput = forwardRef(function ChatInput( } | null>(null); const [model, setModel] = useSelectedModel(); const { profile } = useUserProfile(); - const apiKeys = { - claudeApiKey: profile?.claudeApiKey ?? null, - geminiApiKey: profile?.geminiApiKey ?? null, - }; + const apiKeys = apiKeysFromProfile(profile); const textareaRef = useRef(null); const [docSelectorOpen, setDocSelectorOpen] = useState(false); const [workflowModalOpen, setWorkflowModalOpen] = useState(false); @@ -271,6 +270,7 @@ export const ChatInput = forwardRef(function ChatInput( )} +
    diff --git a/frontend/src/app/components/assistant/EditCard.tsx b/frontend/src/app/components/assistant/EditCard.tsx index ba2ea617..a84db9ff 100644 --- a/frontend/src/app/components/assistant/EditCard.tsx +++ b/frontend/src/app/components/assistant/EditCard.tsx @@ -19,13 +19,6 @@ function findMatch( const byId = container.querySelector( `${tag}[data-w-id="${opts.w_id}"]`, ) as HTMLElement | null; - console.log("[EditCard] findMatch by w_id", { - tag, - w_id: opts.w_id, - found: !!byId, - totalTagged: container.querySelectorAll(`${tag}[data-w-id]`).length, - totalAny: container.querySelectorAll(tag).length, - }); if (byId) return byId; } const text = opts.text ?? ""; @@ -42,12 +35,6 @@ function findMatch( normalizeText(el.textContent ?? "").includes(target), ) ?? null; - console.log("[EditCard] findMatch by text", { - tag, - target, - found: !!byText, - candidateCount: candidates.length, - }); return byText; } @@ -117,13 +104,6 @@ export function applyOptimisticResolution( const scrolls = document.querySelectorAll( `[data-document-id="${CSS.escape(annotation.document_id)}"]`, ); - console.log("[EditCard] optimistic scrolls found:", scrolls.length, { - document_id: annotation.document_id, - ins_w_id: annotation.ins_w_id, - del_w_id: annotation.del_w_id, - inserted_text: annotation.inserted_text?.slice(0, 40), - deleted_text: annotation.deleted_text?.slice(0, 40), - }); scrolls.forEach((scroll) => { const container = scroll.querySelector(".docx-view-container"); if (!container) return; diff --git a/frontend/src/app/components/assistant/McpToggleButton.tsx b/frontend/src/app/components/assistant/McpToggleButton.tsx new file mode 100644 index 00000000..53d09b84 --- /dev/null +++ b/frontend/src/app/components/assistant/McpToggleButton.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { AlertCircle, Plug, Plus } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + listMcpServers, + updateMcpServer, + type McpServer, +} from "@/app/lib/mikeApi"; + +/** + * Sit next to "Documents" / "Workflows" in the chat input. Opens a popover + * where the user toggles each of their configured MCP servers on/off. The + * toggle flips `enabled` on the row, which the chat backend honors at the + * start of the next request. + */ +export function McpToggleButton() { + const [servers, setServers] = useState(null); + const [open, setOpen] = useState(false); + const [busy, setBusy] = useState>({}); + + const reload = useCallback(async () => { + try { + const list = await listMcpServers(); + setServers(list); + } catch { + setServers([]); + } + }, []); + + // Refresh when the menu opens so toggles always reflect current state + // (the user may have edited servers in the settings page). + useEffect(() => { + if (open) reload(); + else if (servers === null) reload(); + }, [open, reload, servers]); + + const handleToggle = async (server: McpServer) => { + setBusy((s) => ({ ...s, [server.id]: true })); + // Optimistic flip. + setServers((prev) => + prev + ? prev.map((s) => + s.id === server.id ? { ...s, enabled: !s.enabled } : s, + ) + : prev, + ); + try { + await updateMcpServer(server.id, { enabled: !server.enabled }); + } catch { + // Revert on error. + await reload(); + } finally { + setBusy((s) => ({ ...s, [server.id]: false })); + } + }; + + // Hide the button entirely when the user has no servers configured — + // surface only emerges when there's something to toggle. + if (servers !== null && servers.length === 0) return null; + + const enabledCount = servers?.filter((s) => s.enabled).length ?? 0; + const totalCount = servers?.length ?? 0; + + return ( + + + + + + + Connectors + + + {servers?.map((s) => ( + handleToggle(s)} + /> + ))} + + + + Manage connectors + + + + ); +} + +function McpRow({ + server, + busy, + onToggle, +}: { + server: McpServer; + busy: boolean; + onToggle: () => void; +}) { + const safeName = + server.name.trim().length > 0 ? server.name.trim() : "Untitled"; + return ( + + ); +} + +function ToggleSwitch({ on }: { on: boolean }) { + return ( + + + + ); +} diff --git a/frontend/src/app/components/assistant/ModelToggle.tsx b/frontend/src/app/components/assistant/ModelToggle.tsx index cc10d518..f9ac42da 100644 --- a/frontend/src/app/components/assistant/ModelToggle.tsx +++ b/frontend/src/app/components/assistant/ModelToggle.tsx @@ -1,115 +1,85 @@ -"use client"; +'use client'; -import { useState } from "react"; -import { ChevronDown, Check, AlertCircle } from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { isModelAvailable } from "@/app/lib/modelAvailability"; +import { useState } from 'react'; +import { ChevronDown, Check, AlertCircle } from 'lucide-react'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { isModelAvailable } from '@/app/lib/modelAvailability'; export interface ModelOption { - id: string; - label: string; - group: "Anthropic" | "Google"; + id: string; + label: string; + group: 'Anthropic' | 'Google' | 'OpenRouter'; } export const MODELS: ModelOption[] = [ - { id: "claude-opus-4-7", label: "Claude Opus 4.7", group: "Anthropic" }, - { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", group: "Anthropic" }, - { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", group: "Google" }, - { id: "gemini-3-flash-preview", label: "Gemini 3 Flash", group: "Google" }, + { id: 'claude-opus-4-7', label: 'Claude Opus 4.7', group: 'Anthropic' }, + { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', group: 'Anthropic' }, + { id: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro', group: 'Google' }, + { id: 'gemini-3-flash-preview', label: 'Gemini 3 Flash', group: 'Google' }, + { id: 'openrouter/openai/gpt-5.3-chat', label: 'GPT-5.3', group: 'OpenRouter' }, + { id: 'openrouter/openai/gpt-4o-mini', label: 'GPT-4o Mini', group: 'OpenRouter' }, + { id: 'openrouter/anthropic/claude-sonnet-4.6', label: 'Claude Sonnet 4.6', group: 'OpenRouter' }, + { id: 'openrouter/anthropic/claude-opus-4.7', label: 'Claude Opus 4.7', group: 'OpenRouter' }, + { id: 'openrouter/x-ai/grok-4.3', label: 'Grok 4.3', group: 'OpenRouter' } ]; -export const DEFAULT_MODEL_ID = "gemini-3-flash-preview"; +export const DEFAULT_MODEL_ID = 'gemini-3-flash-preview'; export const ALLOWED_MODEL_IDS = new Set(MODELS.map((m) => m.id)); -const GROUP_ORDER: ModelOption["group"][] = ["Anthropic", "Google"]; +const GROUP_ORDER: ModelOption['group'][] = ['Anthropic', 'Google', 'OpenRouter']; interface Props { - value: string; - onChange: (id: string) => void; - apiKeys?: { - claudeApiKey: string | null; - geminiApiKey: string | null; - }; + value: string; + onChange: (id: string) => void; + apiKeys?: { + claudeApiKey: string | null; + geminiApiKey: string | null; + openrouterApiKey: string | null; + }; } export function ModelToggle({ value, onChange, apiKeys }: Props) { - const [isOpen, setIsOpen] = useState(false); - const selected = MODELS.find((m) => m.id === value); - const selectedLabel = selected?.label ?? "Model"; - const selectedAvailable = apiKeys - ? isModelAvailable(value, apiKeys) - : true; + const [isOpen, setIsOpen] = useState(false); + const selected = MODELS.find((m) => m.id === value); + const selectedLabel = selected?.label ?? 'Model'; + const selectedAvailable = apiKeys ? isModelAvailable(value, apiKeys) : true; - return ( - - - - - - {GROUP_ORDER.map((group, gi) => { - const items = MODELS.filter((m) => m.group === group); - if (items.length === 0) return null; - return ( -
    - {gi > 0 && } - - {group} - - {items.map((m) => { - const available = apiKeys - ? isModelAvailable(m.id, apiKeys) - : true; - return ( - onChange(m.id)} - > - - {m.label} - - {!available && ( - - )} - {m.id === value && available && ( - - )} - - ); - })} -
    - ); - })} -
    -
    - ); + return ( + + + + + + {GROUP_ORDER.map((group, gi) => { + const items = MODELS.filter((m) => m.group === group); + if (items.length === 0) return null; + return ( +
    + {gi > 0 && } + {group} + {items.map((m) => { + const available = apiKeys ? isModelAvailable(m.id, apiKeys) : true; + return ( + onChange(m.id)}> + {m.label} + {!available && } + {m.id === value && available && } + + ); + })} +
    + ); + })} +
    +
    + ); } diff --git a/frontend/src/app/components/shared/DocxView.tsx b/frontend/src/app/components/shared/DocxView.tsx index 1fc81156..3ce26157 100644 --- a/frontend/src/app/components/shared/DocxView.tsx +++ b/frontend/src/app/components/shared/DocxView.tsx @@ -347,13 +347,6 @@ export function DocxView({ const scrollEl = scrollRef.current; const containerEl = containerRef.current; - console.log("[DocxView] render effect fired", { - documentId, - versionId, - refetchKey, - bytesLen: bytes.byteLength, - }); - // Remember scroll position across re-renders so Accept/Reject stays put. lastScrollTopRef.current = scrollEl.scrollTop; const thisRender = ++renderKeyRef.current; diff --git a/frontend/src/app/components/shared/types.ts b/frontend/src/app/components/shared/types.ts index 2fa4d6dc..475ca4f6 100644 --- a/frontend/src/app/components/shared/types.ts +++ b/frontend/src/app/components/shared/types.ts @@ -85,6 +85,8 @@ export type AssistantEvent = | { type: "tool_call_start"; name: string; + /** Friendly label (e.g. "Legal Data Hunter · search") for MCP tools. */ + display_name?: string; isStreaming?: boolean; } | { type: "thinking"; isStreaming?: boolean } @@ -112,6 +114,17 @@ export type AssistantEvent = isStreaming?: boolean; } | { type: "doc_download"; filename: string; download_url: string } + | { + type: "mcp_tool_result"; + server: string; + tool: string; + ok: boolean; + /** JSON-stringified args (capped server-side). */ + args: string; + /** Tool output text (capped server-side). */ + output: string; + isStreaming?: boolean; + } | { type: "doc_replicated"; /** Source document filename. */ diff --git a/frontend/src/app/components/tabular/TRChatPanel.tsx b/frontend/src/app/components/tabular/TRChatPanel.tsx index 3522df3a..9151d7ff 100644 --- a/frontend/src/app/components/tabular/TRChatPanel.tsx +++ b/frontend/src/app/components/tabular/TRChatPanel.tsx @@ -33,6 +33,7 @@ import { ApiKeyMissingModal } from "../shared/ApiKeyMissingModal"; import { PreResponseWrapper } from "../shared/PreResponseWrapper"; import { useUserProfile } from "@/contexts/UserProfileContext"; import { + apiKeysFromProfile, getModelProvider, isModelAvailable, type ModelProvider, @@ -186,16 +187,26 @@ function TRResponseStatus({ isActive }: { isActive: boolean }) { const wasActiveRef = useRef(false); useEffect(() => { + let hideTimer: ReturnType | null = null; if (wasActiveRef.current && !isActive) { - setShowDone(true); - setDoneVisible(true); - const t = setTimeout(() => setDoneVisible(false), 1500); + const showTimer = setTimeout(() => { + setShowDone(true); + setDoneVisible(true); + hideTimer = setTimeout(() => setDoneVisible(false), 1500); + }, 0); wasActiveRef.current = isActive; - return () => clearTimeout(t); + return () => { + clearTimeout(showTimer); + if (hideTimer) clearTimeout(hideTimer); + }; } if (!wasActiveRef.current && isActive) { - setShowDone(false); - setDoneVisible(false); + const resetTimer = setTimeout(() => { + setShowDone(false); + setDoneVisible(false); + }, 0); + wasActiveRef.current = isActive; + return () => clearTimeout(resetTimer); } wasActiveRef.current = isActive; }, [isActive]); @@ -453,7 +464,7 @@ function TRChatInput({ onCancel: () => void; model: string; onModelChange: (id: string) => void; - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }; + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openrouterApiKey: string | null }; }) { const [value, setValue] = useState(""); const textareaRef = useRef(null); @@ -607,10 +618,7 @@ export function TRChatPanel({ onChatIdChange, }: Props) { const { profile, updateModelPreference } = useUserProfile(); - const apiKeys = { - claudeApiKey: profile?.claudeApiKey ?? null, - geminiApiKey: profile?.geminiApiKey ?? null, - }; + const apiKeys = apiKeysFromProfile(profile); const currentModel = profile?.tabularModel ?? "gemini-3-flash-preview"; const [apiKeyModalProvider, setApiKeyModalProvider] = useState(null); diff --git a/frontend/src/app/components/tabular/TREditColumnMenu.tsx b/frontend/src/app/components/tabular/TREditColumnMenu.tsx index b16ccb56..46b48313 100644 --- a/frontend/src/app/components/tabular/TREditColumnMenu.tsx +++ b/frontend/src/app/components/tabular/TREditColumnMenu.tsx @@ -85,8 +85,6 @@ export function TREditColumnMenu({ setSaving(false); } } - console.log(tags); - async function handleDelete() { setDeleting(true); try { diff --git a/frontend/src/app/components/tabular/TRSidePanel.tsx b/frontend/src/app/components/tabular/TRSidePanel.tsx index 9a6763ab..737d2fc8 100644 --- a/frontend/src/app/components/tabular/TRSidePanel.tsx +++ b/frontend/src/app/components/tabular/TRSidePanel.tsx @@ -109,10 +109,6 @@ export function TRSidePanel({ const { processed: reasoningText, citations: reasoningCitations } = preprocessCitations(cell.content?.reasoning ?? ""); - useEffect(() => { - console.log("[TRSidePanel] summary:", cell.content?.summary ?? ""); - }, [cell.id, cell.content?.summary]); - return (
    - "{docCitation.quote}" + "{docCitation.quote}"

    {(isTruncated || quoteExpanded) && ( (null); const router = useRouter(); const { profile } = useUserProfile(); - const apiKeys = { - claudeApiKey: profile?.claudeApiKey ?? null, - geminiApiKey: profile?.geminiApiKey ?? null, - }; + const apiKeys = apiKeysFromProfile(profile); const tabularModel = profile?.tabularModel ?? "gemini-3-flash-preview"; useEffect(() => { diff --git a/frontend/src/app/hooks/useAssistantChat.ts b/frontend/src/app/hooks/useAssistantChat.ts index fa82ef40..0e04ecf7 100644 --- a/frontend/src/app/hooks/useAssistantChat.ts +++ b/frontend/src/app/hooks/useAssistantChat.ts @@ -534,6 +534,9 @@ export function useAssistantChat({ pushEvent({ type: "tool_call_start", name: (data.name as string) ?? "", + display_name: + (data.display_name as string | undefined) ?? + undefined, isStreaming: true, }); continue; @@ -756,6 +759,19 @@ export function useAssistantChat({ continue; } + if (data.type === "mcp_tool_result") { + pushEvent({ + type: "mcp_tool_result", + server: (data.server as string) ?? "", + tool: (data.tool as string) ?? "", + ok: data.ok !== false, + args: (data.args as string) ?? "", + output: (data.output as string) ?? "", + }); + pushThinkingPlaceholder(); + continue; + } + if (data.type === "citations") { // End-of-stream signal — scrub any lingering // placeholders so they don't persist into the diff --git a/frontend/src/app/hooks/useFetchDocxBytes.ts b/frontend/src/app/hooks/useFetchDocxBytes.ts index 43cdf252..758cb557 100644 --- a/frontend/src/app/hooks/useFetchDocxBytes.ts +++ b/frontend/src/app/hooks/useFetchDocxBytes.ts @@ -47,19 +47,13 @@ export function useFetchDocxBytes( const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - console.log("[useFetchDocxBytes] init", { - documentId, - versionId, - refetchKey, - initialKey, - cacheHit: initialKey ? bytesCache.has(initialKey) : null, - }); - useEffect(() => { if (!documentId) { - setBytes(null); - setDownloadUrl(null); - return; + const resetTimer = setTimeout(() => { + setBytes(null); + setDownloadUrl(null); + }, 0); + return () => clearTimeout(resetTimer); } const key = cacheKey(documentId, versionId, refetchKey); @@ -73,16 +67,21 @@ export function useFetchDocxBytes( // Cache hit: reuse bytes synchronously, no network, no spinner. const cached = bytesCache.get(key); if (cached) { - setBytes(cached); - setDownloadUrl(url); - setLoading(false); - setError(null); - return; + const cacheTimer = setTimeout(() => { + setBytes(cached); + setDownloadUrl(url); + setLoading(false); + setError(null); + }, 0); + return () => clearTimeout(cacheTimer); } let cancelled = false; - setLoading(true); - setError(null); + const loadingTimer = setTimeout(() => { + if (cancelled) return; + setLoading(true); + setError(null); + }, 0); const pending = inFlight.get(key) ?? @@ -120,6 +119,7 @@ export function useFetchDocxBytes( return () => { cancelled = true; + clearTimeout(loadingTimer); }; }, [documentId, versionId, refetchKey]); diff --git a/frontend/src/app/lib/mikeApi.ts b/frontend/src/app/lib/mikeApi.ts index 2d2a7417..abda6082 100644 --- a/frontend/src/app/lib/mikeApi.ts +++ b/frontend/src/app/lib/mikeApi.ts @@ -815,3 +815,81 @@ export async function deleteWorkflowShare( method: "DELETE", }); } + +// --------------------------------------------------------------------------- +// MCP servers +// --------------------------------------------------------------------------- + +export interface McpServer { + id: string; + slug: string; + name: string; + url: string; + header_keys: string[]; + enabled: boolean; + last_error: string | null; + auth_type: "headers" | "oauth"; + oauth_authorized: boolean; + created_at: string; + updated_at: string; +} + +export interface McpServerTestResult { + ok: boolean; + tool_count?: number; + tools?: { name: string; description: string }[]; + error?: string; +} + +export async function listMcpServers(): Promise { + return apiRequest("/user/mcp-servers"); +} + +export async function createMcpServer(payload: { + name: string; + url: string; + slug?: string; + headers?: Record; + enabled?: boolean; + auth_type?: "headers" | "oauth"; +}): Promise { + return apiRequest("/user/mcp-servers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +} + +export async function startMcpOauth( + id: string, +): Promise<{ authorize_url: string | null; already_authorized?: boolean }> { + return apiRequest(`/user/mcp-servers/${id}/oauth/start`, { + method: "POST", + }); +} + +export async function updateMcpServer( + id: string, + payload: { + name?: string; + url?: string; + headers?: Record; + enabled?: boolean; + }, +): Promise { + return apiRequest(`/user/mcp-servers/${id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); +} + +export async function deleteMcpServer(id: string): Promise { + await apiRequest(`/user/mcp-servers/${id}`, { method: "DELETE" }); +} + +export async function testMcpServer(id: string): Promise { + return apiRequest(`/user/mcp-servers/${id}/test`, { + method: "POST", + }); +} diff --git a/frontend/src/app/lib/modelAvailability.ts b/frontend/src/app/lib/modelAvailability.ts index 933a8c2d..b6ed2eb4 100644 --- a/frontend/src/app/lib/modelAvailability.ts +++ b/frontend/src/app/lib/modelAvailability.ts @@ -1,39 +1,60 @@ import { MODELS, type ModelOption } from "../components/assistant/ModelToggle"; +import type { UserProfile } from "@/contexts/UserProfileContext"; -export type ModelProvider = "claude" | "gemini"; +export type ModelProvider = "claude" | "gemini" | "openrouter"; export function getModelProvider(modelId: string): ModelProvider | null { const model = MODELS.find((m) => m.id === modelId); if (!model) return null; - return model.group === "Anthropic" ? "claude" : "gemini"; + if (model.group === "Anthropic") return "claude"; + if (model.group === "Google") return "gemini"; + if (model.group === "OpenRouter") return "openrouter"; + return null; } export function isModelAvailable( modelId: string, - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }, + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openrouterApiKey?: string | null }, ): boolean { const provider = getModelProvider(modelId); if (!provider) return false; - return provider === "claude" - ? !!apiKeys.claudeApiKey?.trim() - : !!apiKeys.geminiApiKey?.trim(); + if (provider === "claude") return !!apiKeys.claudeApiKey?.trim(); + if (provider === "gemini") return !!apiKeys.geminiApiKey?.trim(); + if (provider === "openrouter") return !!apiKeys.openrouterApiKey?.trim(); + return false; } export function isProviderAvailable( provider: ModelProvider, - apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null }, + apiKeys: { claudeApiKey: string | null; geminiApiKey: string | null; openrouterApiKey?: string | null }, ): boolean { - return provider === "claude" - ? !!apiKeys.claudeApiKey?.trim() - : !!apiKeys.geminiApiKey?.trim(); + if (provider === "claude") return !!apiKeys.claudeApiKey?.trim(); + if (provider === "gemini") return !!apiKeys.geminiApiKey?.trim(); + if (provider === "openrouter") return !!apiKeys.openrouterApiKey?.trim(); + return false; } export function providerLabel(provider: ModelProvider): string { - return provider === "claude" ? "Anthropic (Claude)" : "Google (Gemini)"; + if (provider === "claude") return "Anthropic (Claude)"; + if (provider === "gemini") return "Google (Gemini)"; + if (provider === "openrouter") return "OpenRouter"; + return ""; +} + +export function apiKeysFromProfile(profile: UserProfile | null) { + return { + claudeApiKey: profile?.hasClaudeApiKey ? ("configured" as const) : null, + geminiApiKey: profile?.hasGeminiApiKey ? ("configured" as const) : null, + openrouterApiKey: profile?.hasOpenrouterApiKey + ? ("configured" as const) + : null, + }; } export function modelGroupToProvider( group: ModelOption["group"], ): ModelProvider { - return group === "Anthropic" ? "claude" : "gemini"; + if (group === "Anthropic") return "claude"; + if (group === "Google") return "gemini"; + return "openrouter"; } diff --git a/frontend/src/app/signup/page.tsx b/frontend/src/app/signup/page.tsx index 21ad9473..ca3c17c8 100644 --- a/frontend/src/app/signup/page.tsx +++ b/frontend/src/app/signup/page.tsx @@ -59,32 +59,32 @@ export default function SignupPage() { const trimmedName = name.trim(); const trimmedOrg = organisation.trim(); if (trimmedName || trimmedOrg) { - // The handle_new_user DB trigger creates the - // user_profiles row synchronously on auth.users insert, - // so we UPDATE rather than upsert — RLS permits update - // of the user's own row but blocks self-INSERT. - const { error: profileError } = await supabase - .from("user_profiles") - .update({ + const apiBase = + process.env.NEXT_PUBLIC_API_BASE_URL ?? + "http://localhost:3001"; + await fetch(`${apiBase}/user/profile`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${data.session.access_token}`, + }, + body: JSON.stringify({ ...(trimmedName && { display_name: trimmedName }), ...(trimmedOrg && { organisation: trimmedOrg }), - updated_at: new Date().toISOString(), - }) - .eq("user_id", data.session.user.id); - if (profileError) { - console.error( - "[signup] failed to persist profile fields", - profileError, - ); - } + }), + }).catch(() => {}); } } setSuccess(true); setTimeout(() => { router.push("/assistant"); }, 2000); - } catch (error: any) { - setError(error.message || "An error occurred during signup"); + } catch (error: unknown) { + setError( + error instanceof Error + ? error.message + : "An error occurred during signup", + ); } finally { setLoading(false); } diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index d2078a9b..a13b58be 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -34,9 +34,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { await fetch(`${apiBase}/user/profile`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}` }, - }).catch((e) => { - console.log(e); - }); + }).catch(() => {}); }; const checkUser = async () => { @@ -49,7 +47,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { id: session.user.id, email: session.user.email || "", }); - ensureProfile(session.access_token); + await ensureProfile(session.access_token); } setAuthLoading(false); }; @@ -64,7 +62,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { id: session.user.id, email: session.user.email || "", }); - ensureProfile(session.access_token); + await ensureProfile(session.access_token); } else { setUser(null); } diff --git a/frontend/src/contexts/UserProfileContext.tsx b/frontend/src/contexts/UserProfileContext.tsx index 12061076..7efe9edb 100644 --- a/frontend/src/contexts/UserProfileContext.tsx +++ b/frontend/src/contexts/UserProfileContext.tsx @@ -11,7 +11,23 @@ import React, { import { supabase } from "@/lib/supabase"; import { useAuth } from "@/contexts/AuthContext"; -interface UserProfile { +const API_BASE = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:3001"; +const MONTHLY_CREDIT_LIMIT = 999999; + +interface ServerProfile { + display_name: string | null; + organisation: string | null; + message_credits_used: number; + credits_reset_date: string; + tier: string; + tabular_model: string; + has_claude_api_key: boolean; + has_gemini_api_key: boolean; + has_openrouter_api_key: boolean; +} + +export interface UserProfile { displayName: string | null; organisation: string | null; messageCreditsUsed: number; @@ -19,8 +35,9 @@ interface UserProfile { creditsRemaining: number; tier: string; tabularModel: string; - claudeApiKey: string | null; - geminiApiKey: string | null; + hasClaudeApiKey: boolean; + hasGeminiApiKey: boolean; + hasOpenrouterApiKey: boolean; } interface UserProfileContextType { @@ -33,7 +50,7 @@ interface UserProfileContextType { value: string, ) => Promise; updateApiKey: ( - provider: "claude" | "gemini", + provider: "claude" | "gemini" | "openrouter", value: string | null, ) => Promise; reloadProfile: () => Promise; @@ -44,111 +61,77 @@ const UserProfileContext = createContext( undefined, ); +async function getAuthHeaders(): Promise> { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session?.access_token + ? { Authorization: `Bearer ${session.access_token}` } + : {}; +} + +function fallbackProfile(): UserProfile { + const reset = new Date(); + reset.setDate(reset.getDate() + 30); + return { + displayName: null, + organisation: null, + messageCreditsUsed: 0, + creditsResetDate: reset.toISOString(), + creditsRemaining: MONTHLY_CREDIT_LIMIT, + tier: "Free", + tabularModel: "gemini-3-flash-preview", + hasClaudeApiKey: false, + hasGeminiApiKey: false, + hasOpenrouterApiKey: false, + }; +} + +function mapProfile(data: ServerProfile): UserProfile { + const creditsUsed = data.message_credits_used ?? 0; + return { + displayName: data.display_name, + organisation: data.organisation ?? null, + messageCreditsUsed: creditsUsed, + creditsResetDate: data.credits_reset_date, + creditsRemaining: MONTHLY_CREDIT_LIMIT - creditsUsed, + tier: data.tier || "Free", + tabularModel: data.tabular_model || "gemini-3-flash-preview", + hasClaudeApiKey: !!data.has_claude_api_key, + hasGeminiApiKey: !!data.has_gemini_api_key, + hasOpenrouterApiKey: !!data.has_openrouter_api_key, + }; +} + +async function profileRequest( + method: "GET" | "PATCH", + body?: Record, +): Promise { + const headers = await getAuthHeaders(); + const response = await fetch(`${API_BASE}/user/profile`, { + method, + cache: "no-store", + headers: { + Accept: "application/json", + ...(body ? { "Content-Type": "application/json" } : {}), + ...headers, + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!response.ok) throw new Error(await response.text()); + return mapProfile((await response.json()) as ServerProfile); +} + export function UserProfileProvider({ children }: { children: ReactNode }) { const { user, isAuthenticated } = useAuth(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); - const loadProfile = useCallback(async (userId: string) => { + const loadProfile = useCallback(async () => { try { - const { data, error } = await supabase - .from("user_profiles") - .select("*") - .eq("user_id", userId) - .single(); - - // Define credit limit constant - const MONTHLY_CREDIT_LIMIT = 999999; // temporarily unlimited - - // Calculate a default future reset date (30 days from now) - const futureResetDate = new Date(); - futureResetDate.setDate(futureResetDate.getDate() + 30); - const defaultResetDateStr = futureResetDate.toISOString(); - - if (error) { - // Set fallback profile data if profile doesn't exist - setProfile({ - displayName: null, - organisation: null, - messageCreditsUsed: 0, - creditsResetDate: defaultResetDateStr, - creditsRemaining: MONTHLY_CREDIT_LIMIT, - tier: "Free", - tabularModel: "gemini-3-flash-preview", - claudeApiKey: null, - geminiApiKey: null, - }); - return; - } - - // Use fetched data to update profile state - if (data) { - let creditsUsed = data.message_credits_used; - let resetDate = data.credits_reset_date; - let creditsRemaining = MONTHLY_CREDIT_LIMIT - creditsUsed; - let shouldUpdateDb = false; - - // Check if credits have expired and need reset - if (resetDate && new Date() > new Date(resetDate)) { - // Calculate new reset date - const newResetDate = new Date(); - newResetDate.setDate(newResetDate.getDate() + 30); - resetDate = newResetDate.toISOString(); - creditsUsed = 0; - creditsRemaining = MONTHLY_CREDIT_LIMIT; - shouldUpdateDb = true; - } - - // 1. Update local state immediately - setProfile({ - displayName: data.display_name, - organisation: data.organisation ?? null, - messageCreditsUsed: creditsUsed, - creditsResetDate: resetDate, - creditsRemaining: creditsRemaining, - tier: data.tier || "Free", - tabularModel: - data.tabular_model || "gemini-3-flash-preview", - claudeApiKey: data.claude_api_key ?? null, - geminiApiKey: data.gemini_api_key ?? null, - }); - - // 2. Update database in background if needed - if (shouldUpdateDb) { - supabase - .from("user_profiles") - .update({ - message_credits_used: 0, - credits_reset_date: resetDate, - updated_at: new Date().toISOString(), - }) - .eq("user_id", userId) - .then(({ error }) => { - if (error) - console.error( - "Failed to auto-reset credits", - error, - ); - }); - } - } - } catch (e) { - // Calculate a default future reset date for fallback - const futureResetDate = new Date(); - futureResetDate.setDate(futureResetDate.getDate() + 30); - - // Set fallback profile data on exception - setProfile({ - displayName: null, - organisation: null, - messageCreditsUsed: 0, - creditsResetDate: futureResetDate.toISOString(), - creditsRemaining: 999999, // temporarily unlimited - tier: "Free", - tabularModel: "gemini-3-flash-preview", - claudeApiKey: null, - geminiApiKey: null, - }); + setProfile(await profileRequest("GET")); + } catch { + setProfile(fallbackProfile()); } finally { setLoading(false); } @@ -157,170 +140,57 @@ export function UserProfileProvider({ children }: { children: ReactNode }) { useEffect(() => { if (isAuthenticated && user) { setLoading(true); - loadProfile(user.id); + loadProfile(); } else { setProfile(null); setLoading(false); } }, [isAuthenticated, user, loadProfile]); - const updateDisplayName = useCallback( - async (displayName: string): Promise => { - if (!user) { - return false; - } - - try { - const { error } = await supabase - .from("user_profiles") - .update({ - display_name: displayName, - updated_at: new Date().toISOString(), - }) - .eq("user_id", user.id); - - if (error) { - throw error; - } + const patchProfile = useCallback(async (body: Record) => { + try { + const next = await profileRequest("PATCH", body); + setProfile(next); + return true; + } catch { + return false; + } + }, []); - setProfile((prev) => (prev ? { ...prev, displayName } : null)); - return true; - } catch { - return false; - } - }, - [user], + const updateDisplayName = useCallback( + async (displayName: string): Promise => + patchProfile({ display_name: displayName }), + [patchProfile], ); const updateOrganisation = useCallback( - async (organisation: string): Promise => { - if (!user) return false; - try { - const { error } = await supabase - .from("user_profiles") - .update({ - organisation, - updated_at: new Date().toISOString(), - }) - .eq("user_id", user.id); - if (error) throw error; - setProfile((prev) => - prev ? { ...prev, organisation } : null, - ); - return true; - } catch { - return false; - } - }, - [user], + async (organisation: string): Promise => + patchProfile({ organisation }), + [patchProfile], ); const updateModelPreference = useCallback( - async ( - field: "tabularModel", - value: string, - ): Promise => { - if (!user) return false; - const dbField = field === "tabularModel" ? "tabular_model" : ""; - if (!dbField) return false; - try { - const { error } = await supabase - .from("user_profiles") - .update({ - [dbField]: value, - updated_at: new Date().toISOString(), - }) - .eq("user_id", user.id); - if (error) throw error; - setProfile((prev) => - prev ? { ...prev, [field]: value } : null, - ); - return true; - } catch { - return false; - } - }, - [user], + async (_field: "tabularModel", value: string): Promise => + patchProfile({ tabular_model: value }), + [patchProfile], ); const updateApiKey = useCallback( async ( - provider: "claude" | "gemini", + provider: "claude" | "gemini" | "openrouter", value: string | null, - ): Promise => { - if (!user) return false; - const dbField = - provider === "claude" ? "claude_api_key" : "gemini_api_key"; - const stateField = - provider === "claude" ? "claudeApiKey" : "geminiApiKey"; - const normalized = value?.trim() ? value.trim() : null; - try { - const { error } = await supabase - .from("user_profiles") - .update({ - [dbField]: normalized, - updated_at: new Date().toISOString(), - }) - .eq("user_id", user.id); - if (error) throw error; - setProfile((prev) => - prev ? { ...prev, [stateField]: normalized } : null, - ); - return true; - } catch { - return false; - } - }, - [user], + ): Promise => patchProfile({ api_keys: { [provider]: value } }), + [patchProfile], ); const reloadProfile = useCallback(async () => { - if (user) { - await loadProfile(user.id); - } + if (user) await loadProfile(); }, [user, loadProfile]); const incrementMessageCredits = useCallback(async (): Promise => { - if (!user || !profile) { - return false; - } - - // Check if user has credits remaining - if (profile.creditsRemaining <= 0) { - return false; - } - - try { - const newCreditsUsed = profile.messageCreditsUsed + 1; - - const { error } = await supabase - .from("user_profiles") - .update({ - message_credits_used: newCreditsUsed, - updated_at: new Date().toISOString(), - }) - .eq("user_id", user.id); - - if (error) { - throw error; - } - - // Update local state - setProfile((prev) => - prev - ? { - ...prev, - messageCreditsUsed: newCreditsUsed, - creditsRemaining: 999999 - newCreditsUsed, // temporarily unlimited - } - : null, - ); - - return true; - } catch (err) { - return false; - } - }, [user, profile]); + if (!user || !profile || profile.creditsRemaining <= 0) return false; + return patchProfile({ increment_message_credits: true }); + }, [user, profile, patchProfile]); return ( .r2.cloudflarestorage.com - * R2_ACCESS_KEY_ID — R2 API token (Access Key ID) - * R2_SECRET_ACCESS_KEY — R2 API token (Secret Access Key) - * R2_BUCKET_NAME — bucket name (default: "mike") - */ - -import { - S3Client, - PutObjectCommand, - GetObjectCommand, - DeleteObjectCommand, -} from "@aws-sdk/client-s3"; -import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner"; - -function getClient(): S3Client { - return new S3Client({ - region: "auto", - endpoint: process.env.R2_ENDPOINT_URL!, - credentials: { - accessKeyId: process.env.R2_ACCESS_KEY_ID!, - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, - }, - }); -} - -const BUCKET = process.env.R2_BUCKET_NAME ?? "mike"; - -export const storageEnabled = Boolean( - process.env.R2_ENDPOINT_URL && - process.env.R2_ACCESS_KEY_ID && - process.env.R2_SECRET_ACCESS_KEY, -); - -// --------------------------------------------------------------------------- -// Upload -// --------------------------------------------------------------------------- - -export async function uploadFile( - key: string, - content: ArrayBuffer, - contentType: string, -): Promise { - const client = getClient(); - await client.send( - new PutObjectCommand({ - Bucket: BUCKET, - Key: key, - Body: Buffer.from(content), - ContentType: contentType, - }), - ); -} - -// --------------------------------------------------------------------------- -// Download -// --------------------------------------------------------------------------- - -export async function downloadFile(key: string): Promise { - if (!storageEnabled) return null; - try { - const client = getClient(); - const response = await client.send( - new GetObjectCommand({ Bucket: BUCKET, Key: key }), - ); - if (!response.Body) return null; - const bytes = await response.Body.transformToByteArray(); - return bytes.buffer as ArrayBuffer; - } catch { - return null; - } -} - -// --------------------------------------------------------------------------- -// Delete -// --------------------------------------------------------------------------- - -export async function deleteFile(key: string): Promise { - if (!storageEnabled) return; - const client = getClient(); - await client.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: key })); -} - -// --------------------------------------------------------------------------- -// Signed URL (pre-signed for temporary direct access) -// --------------------------------------------------------------------------- - -export async function getSignedUrl( - key: string, - expiresIn = 3600, -): Promise { - if (!storageEnabled) return null; - try { - const client = getClient(); - const command = new GetObjectCommand({ Bucket: BUCKET, Key: key }); - return await awsGetSignedUrl(client, command, { expiresIn }); - } catch { - return null; - } -} - -// --------------------------------------------------------------------------- -// Storage key helpers -// --------------------------------------------------------------------------- - -export function storageKey( - userId: string, - docId: string, - filename: string, -): string { - return `documents/${userId}/${docId}/${filename}`; -} - -export function pdfStorageKey( - userId: string, - docId: string, - stem: string, -): string { - return `documents/${userId}/${docId}/${stem}.pdf`; -} - -export function generatedDocKey( - userId: string, - docId: string, - filename: string, -): string { - return `generated/${userId}/${docId}/${filename}`; -} diff --git a/frontend/src/lib/supabase-server.ts b/frontend/src/lib/supabase-server.ts deleted file mode 100644 index 74b159b8..00000000 --- a/frontend/src/lib/supabase-server.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createClient } from "@supabase/supabase-js"; - -/** - * Server-side Supabase client using the service role key. - * Bypasses RLS — only use in API routes after verifying the user. - */ -export function createServerSupabase() { - const url = process.env.NEXT_PUBLIC_SUPABASE_URL || ""; - const key = process.env.SUPABASE_SECRET_KEY || ""; - return createClient(url, key, { auth: { persistSession: false } }); -} - -/** - * Extract and verify the Supabase JWT from the Authorization header. - * Returns the user's UUID string, or throws a Response with 401. - */ -export async function getUserIdFromRequest(req: Request): Promise { - const auth = req.headers.get("authorization") ?? ""; - if (!auth.startsWith("Bearer ")) { - throw new Response("Missing or invalid Authorization header", { status: 401 }); - } - const token = auth.slice(7).trim(); - - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ""; - const serviceKey = process.env.SUPABASE_SECRET_KEY || ""; - - if (!supabaseUrl || !serviceKey) { - // Dev fallback — accept raw token as user ID - return token; - } - - const admin = createClient(supabaseUrl, serviceKey, { auth: { persistSession: false } }); - const { data } = await admin.auth.getUser(token); - if (!data.user) { - throw new Response("Invalid or expired token", { status: 401 }); - } - return data.user.id; -} diff --git a/scripts/generate-secrets.sh b/scripts/generate-secrets.sh index 5c89a04c..d75fcab4 100755 --- a/scripts/generate-secrets.sh +++ b/scripts/generate-secrets.sh @@ -76,6 +76,8 @@ ensure_random_hex AUTHENTICATOR_PASSWORD 24 ensure_random_hex GARAGE_RPC_SECRET 32 ensure_random_hex GARAGE_ADMIN_TOKEN 32 ensure_random_hex JWT_SECRET 32 +ensure_random_hex DOWNLOAD_SIGNING_SECRET 32 +ensure_random_hex USER_API_KEYS_ENCRYPTION_KEY 32 # JWTs depend on JWT_SECRET; regenerate them whenever JWT_SECRET changed # (i.e. when the user runs --force) or when they're empty.