From 4689cdb99a7f7d1e79f967307eb79b977a380f36 Mon Sep 17 00:00:00 2001 From: raphael-hoogvliets Date: Thu, 14 May 2026 14:23:37 +0200 Subject: [PATCH] disclose database migrations and supabase config --- backend/.env.example | 33 +- backend/migrations/0000_baseline.ts | 19 + .../migrations/0001_auth_user_lookup_rpcs.ts | 42 + .../migrations/0002_pdf_conversion_status.ts | 16 + .../0003_uuid_fk_billing_cleanup.ts | 115 + .../0004_select_review_doc_counts.ts | 30 + backend/migrations/0005_rls_policies.ts | 638 ++++ .../0006_workflow_shares_with_check.ts | 72 + backend/migrations/0007_encrypt_api_keys.ts | 31 + .../0008_soft_delete_user_profiles.ts | 16 + .../migrations/0009_account_deletion_jobs.ts | 29 + backend/migrations/000_one_shot_schema.sql | 898 ++++++ backend/package-lock.json | 2690 ++++++++++++++++- backend/package.json | 38 +- backend/schema.sql | 367 --- supabase/.gitignore | 8 + supabase/config.toml | 412 +++ 17 files changed, 4937 insertions(+), 517 deletions(-) create mode 100644 backend/migrations/0000_baseline.ts create mode 100644 backend/migrations/0001_auth_user_lookup_rpcs.ts create mode 100644 backend/migrations/0002_pdf_conversion_status.ts create mode 100644 backend/migrations/0003_uuid_fk_billing_cleanup.ts create mode 100644 backend/migrations/0004_select_review_doc_counts.ts create mode 100644 backend/migrations/0005_rls_policies.ts create mode 100644 backend/migrations/0006_workflow_shares_with_check.ts create mode 100644 backend/migrations/0007_encrypt_api_keys.ts create mode 100644 backend/migrations/0008_soft_delete_user_profiles.ts create mode 100644 backend/migrations/0009_account_deletion_jobs.ts create mode 100644 backend/migrations/000_one_shot_schema.sql delete mode 100644 backend/schema.sql create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml diff --git a/backend/.env.example b/backend/.env.example index 6b4d56150..63db31750 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,12 +1,12 @@ 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 SUPABASE_URL=https://your-project.supabase.co SUPABASE_SECRET_KEY=your-supabase-service-role-key +# Required for cross-tenant test suite (npm run test:cross-tenant) to sign test +# users into anon-key sessions and obtain real JWTs. Optional for runtime. +SUPABASE_ANON_KEY= + +DOWNLOAD_SIGNING_SECRET=your-random-signing-secret-min-32-chars R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com R2_ACCESS_KEY_ID=your-r2-access-key @@ -15,6 +15,25 @@ R2_BUCKET_NAME=mike GEMINI_API_KEY=your-gemini-key ANTHROPIC_API_KEY=your-anthropic-key -OPENAI_API_KEY=your-openai-key + +# Optional — when set, enables raw LLM stream console logging (debug only; remove in production) +LLM_STREAM_DEBUG= + +OPENROUTER_API_KEY=your-openrouter-key RESEND_API_KEY=your-resend-key -USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret + +# Migration runner — Supabase direct connection, NOT the pgBouncer pooler. +# Format: postgresql://postgres:@db..supabase.co:5432/postgres +DATABASE_URL= + +# LLM rate limiting (per-user, applies to all LLM-spending routes) +RATE_LIMIT_WINDOW_MS=60000 # Sliding window in milliseconds (default: 60000 = 1 minute) +RATE_LIMIT_MAX=20 # Max LLM requests per user per window (default: 20) + +# CLEAN-05 — at-rest encryption of user LLM API keys (AES-256-GCM) +# Generate with: openssl rand -hex 32 +HUGO_MASTER_KEY= + +# CLEAN-44 — HMAC secret for account-restore tokens (30-day soft-delete window) +# Generate with: openssl rand -base64 48 +HUGO_RESTORE_TOKEN_SECRET= diff --git a/backend/migrations/0000_baseline.ts b/backend/migrations/0000_baseline.ts new file mode 100644 index 000000000..ff69616c5 --- /dev/null +++ b/backend/migrations/0000_baseline.ts @@ -0,0 +1,19 @@ +/** + * Baseline migration marker. + * + * The schema has already been applied via backend/migrations/000_one_shot_schema.sql. + * This file's only purpose is to give node-pg-migrate a tracked starting point; + * up/down are intentionally no-ops. Future schema changes ship as new + * timestamped migration files in this directory. + */ +import type { MigrationBuilder } from "node-pg-migrate"; + +export const shorthands = undefined; + +export const up = (_pgm: MigrationBuilder): void => { + // No-op: baseline tracks the post-one-shot schema state. +}; + +export const down = (_pgm: MigrationBuilder): void => { + // No-op: baseline cannot be rolled back. +}; diff --git a/backend/migrations/0001_auth_user_lookup_rpcs.ts b/backend/migrations/0001_auth_user_lookup_rpcs.ts new file mode 100644 index 000000000..b2465fef6 --- /dev/null +++ b/backend/migrations/0001_auth_user_lookup_rpcs.ts @@ -0,0 +1,42 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +export async function up(pgm: MigrationBuilder): Promise { + pgm.sql(` + create or replace function public.get_auth_user_by_email(p_email text) + returns table (id uuid, email text) + language sql + security definer + set search_path = '' + as $$ + select u.id, u.email + from auth.users u + where lower(u.email) = lower(p_email) + limit 1; + $$; + + revoke all on function public.get_auth_user_by_email(text) from public, anon, authenticated; + grant execute on function public.get_auth_user_by_email(text) to service_role; + + create or replace function public.get_auth_user_by_id(p_id uuid) + returns table (id uuid, email text) + language sql + security definer + set search_path = '' + as $$ + select u.id, u.email + from auth.users u + where u.id = p_id + limit 1; + $$; + + revoke all on function public.get_auth_user_by_id(uuid) from public, anon, authenticated; + grant execute on function public.get_auth_user_by_id(uuid) to service_role; + `); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.sql(` + drop function if exists public.get_auth_user_by_email(text); + drop function if exists public.get_auth_user_by_id(uuid); + `); +} diff --git a/backend/migrations/0002_pdf_conversion_status.ts b/backend/migrations/0002_pdf_conversion_status.ts new file mode 100644 index 000000000..daa4be103 --- /dev/null +++ b/backend/migrations/0002_pdf_conversion_status.ts @@ -0,0 +1,16 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +export async function up(pgm: MigrationBuilder): Promise { + pgm.addColumns("documents", { + pdf_conversion_status: { + type: "text", + notNull: true, + default: "ok", + check: "pdf_conversion_status IN ('pending', 'ok', 'failed')", + }, + }); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropColumns("documents", ["pdf_conversion_status"]); +} diff --git a/backend/migrations/0003_uuid_fk_billing_cleanup.ts b/backend/migrations/0003_uuid_fk_billing_cleanup.ts new file mode 100644 index 000000000..b6db46a59 --- /dev/null +++ b/backend/migrations/0003_uuid_fk_billing_cleanup.ts @@ -0,0 +1,115 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +export async function up(pgm: MigrationBuilder): Promise { + // 1. Drop indexes covering user_id columns (Postgres requires this before ALTER COLUMN TYPE) + pgm.sql("DROP INDEX IF EXISTS public.idx_projects_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_documents_user_project"); + pgm.sql("DROP INDEX IF EXISTS public.idx_workflows_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_hidden_workflows_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_chats_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_tabular_reviews_user"); + pgm.sql("DROP INDEX IF EXISTS public.tabular_review_chats_user_idx"); + + // 2. Drop composite UNIQUE on hidden_workflows (Postgres blocks type change of constrained column — Pitfall 4) + pgm.sql("ALTER TABLE public.hidden_workflows DROP CONSTRAINT IF EXISTS hidden_workflows_user_id_workflow_id_key"); + + // 3. Alter NOT NULL user_id columns to uuid + add FK CASCADE + const notNullTables = [ + "projects", + "project_subfolders", + "documents", + "chats", + "tabular_reviews", + "tabular_review_chats", + "hidden_workflows", + ]; + for (const t of notNullTables) { + pgm.sql(`ALTER TABLE public.${t} ALTER COLUMN user_id TYPE uuid USING user_id::uuid`); + pgm.sql(`ALTER TABLE public.${t} ADD CONSTRAINT ${t}_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE`); + } + + // 4. workflows.user_id is NULLABLE (Pitfall 3) — keep nullable, FK MATCH SIMPLE allows NULL + pgm.sql("ALTER TABLE public.workflows ALTER COLUMN user_id TYPE uuid USING user_id::uuid"); + pgm.sql("ALTER TABLE public.workflows ADD CONSTRAINT workflows_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE"); + + // 5. workflow_shares.shared_by_user_id (NOT NULL) + pgm.sql("ALTER TABLE public.workflow_shares ALTER COLUMN shared_by_user_id TYPE uuid USING shared_by_user_id::uuid"); + pgm.sql("ALTER TABLE public.workflow_shares ADD CONSTRAINT workflow_shares_shared_by_user_id_fkey FOREIGN KEY (shared_by_user_id) REFERENCES auth.users(id) ON DELETE CASCADE"); + + // 6. Re-add hidden_workflows composite UNIQUE + pgm.sql("ALTER TABLE public.hidden_workflows ADD CONSTRAINT hidden_workflows_user_id_workflow_id_key UNIQUE (user_id, workflow_id)"); + + // 7. Re-create dropped indexes + pgm.sql("CREATE INDEX IF NOT EXISTS idx_projects_user ON public.projects(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_documents_user_project ON public.documents(user_id, project_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_workflows_user ON public.workflows(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_hidden_workflows_user ON public.hidden_workflows(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_chats_user ON public.chats(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_tabular_reviews_user ON public.tabular_reviews(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS tabular_review_chats_user_idx ON public.tabular_review_chats(user_id)"); + + // 8. UNIQUE constraint on document_versions (backs CLEAN-08 retry pattern) + pgm.sql("ALTER TABLE public.document_versions ADD CONSTRAINT document_versions_doc_version_unique UNIQUE (document_id, version_number)"); + + // 9. Drop dead billing columns (CLEAN-48 — out of scope per REQUIREMENTS.md) + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS tier"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS message_credits_used"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS credits_reset_date"); +} + +export async function down(pgm: MigrationBuilder): Promise { + // 1. Restore billing columns + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS tier text NOT NULL DEFAULT 'Free'"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS message_credits_used integer NOT NULL DEFAULT 0"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS credits_reset_date timestamptz NOT NULL DEFAULT (now() + interval '30 days')"); + + // 2. Drop UNIQUE on document_versions + pgm.sql("ALTER TABLE public.document_versions DROP CONSTRAINT IF EXISTS document_versions_doc_version_unique"); + + // 3. Drop re-created indexes + pgm.sql("DROP INDEX IF EXISTS public.idx_projects_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_documents_user_project"); + pgm.sql("DROP INDEX IF EXISTS public.idx_workflows_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_hidden_workflows_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_chats_user"); + pgm.sql("DROP INDEX IF EXISTS public.idx_tabular_reviews_user"); + pgm.sql("DROP INDEX IF EXISTS public.tabular_review_chats_user_idx"); + + // 4. Drop hidden_workflows composite UNIQUE constraint (recreated as text-based below) + pgm.sql("ALTER TABLE public.hidden_workflows DROP CONSTRAINT IF EXISTS hidden_workflows_user_id_workflow_id_key"); + + // 5. Drop FK constraints and revert uuid columns back to text (not null tables) + const notNullTables = [ + "projects", + "project_subfolders", + "documents", + "chats", + "tabular_reviews", + "tabular_review_chats", + "hidden_workflows", + ]; + for (const t of notNullTables) { + pgm.sql(`ALTER TABLE public.${t} DROP CONSTRAINT IF EXISTS ${t}_user_id_fkey`); + pgm.sql(`ALTER TABLE public.${t} ALTER COLUMN user_id TYPE text USING user_id::text`); + } + + // 6. Drop FK and revert workflows.user_id (nullable) + pgm.sql("ALTER TABLE public.workflows DROP CONSTRAINT IF EXISTS workflows_user_id_fkey"); + pgm.sql("ALTER TABLE public.workflows ALTER COLUMN user_id TYPE text USING user_id::text"); + + // 7. Drop FK and revert workflow_shares.shared_by_user_id + pgm.sql("ALTER TABLE public.workflow_shares DROP CONSTRAINT IF EXISTS workflow_shares_shared_by_user_id_fkey"); + pgm.sql("ALTER TABLE public.workflow_shares ALTER COLUMN shared_by_user_id TYPE text USING shared_by_user_id::text"); + + // 8. Re-add hidden_workflows composite UNIQUE (now text-based) + pgm.sql("ALTER TABLE public.hidden_workflows ADD CONSTRAINT hidden_workflows_user_id_workflow_id_key UNIQUE (user_id, workflow_id)"); + + // 9. Restore indexes + pgm.sql("CREATE INDEX IF NOT EXISTS idx_projects_user ON public.projects(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_documents_user_project ON public.documents(user_id, project_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_workflows_user ON public.workflows(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_hidden_workflows_user ON public.hidden_workflows(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_chats_user ON public.chats(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS idx_tabular_reviews_user ON public.tabular_reviews(user_id)"); + pgm.sql("CREATE INDEX IF NOT EXISTS tabular_review_chats_user_idx ON public.tabular_review_chats(user_id)"); +} diff --git a/backend/migrations/0004_select_review_doc_counts.ts b/backend/migrations/0004_select_review_doc_counts.ts new file mode 100644 index 000000000..492090179 --- /dev/null +++ b/backend/migrations/0004_select_review_doc_counts.ts @@ -0,0 +1,30 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +export async function up(pgm: MigrationBuilder): Promise { + pgm.sql(` + -- CLEAN-28: server-side aggregation for tabular review document counts. + -- EXPLAIN confirms idx_tabular_cells_review (review_id, document_id, column_index) + -- covers the (review_id, document_id) prefix — no new index needed. + CREATE OR REPLACE FUNCTION public.select_review_doc_counts(review_ids uuid[]) + RETURNS TABLE (review_id uuid, doc_count bigint) + LANGUAGE sql + STABLE + SECURITY DEFINER + SET search_path = '' + AS $$ + SELECT review_id, count(DISTINCT document_id) AS doc_count + FROM public.tabular_cells + WHERE review_id = ANY(review_ids) + GROUP BY review_id; + $$; + + REVOKE ALL ON FUNCTION public.select_review_doc_counts(uuid[]) FROM public, anon, authenticated; + GRANT EXECUTE ON FUNCTION public.select_review_doc_counts(uuid[]) TO service_role; + `); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.sql(` + DROP FUNCTION IF EXISTS public.select_review_doc_counts(uuid[]); + `); +} diff --git a/backend/migrations/0005_rls_policies.ts b/backend/migrations/0005_rls_policies.ts new file mode 100644 index 000000000..54b4cf5e7 --- /dev/null +++ b/backend/migrations/0005_rls_policies.ts @@ -0,0 +1,638 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +// CLEAN-47: RLS defense-in-depth. +// - 5 SECURITY DEFINER helper functions (language sql + STABLE so the planner inlines and uses GIN). +// - Enable RLS on every user-owned table (14 total — includes tabular_review_chats* per RESEARCH §10 RESOLVED). +// - Closed-by-default policies (owner-only mutations; share-aware SELECT for projects/tabular_reviews/workflows). +// - Parallel jsonb_path_ops GIN indexes for faster @> lookups (research §5). +// Backend continues to use service_role JWT which bypasses RLS — RLS is the floor for anon-key paths. + +export const shorthands = undefined; + +export async function up(pgm: MigrationBuilder): Promise { + // ── SECTION 1: Helper functions ──────────────────────────────────────────── + // Each helper is language sql (NOT plpgsql) so the planner can inline it and + // push the @> filter down into the GIN index. + // See: RESEARCH.md §2 Pitfall 1, Pattern 1. + + // -- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + // Helper 1: is_project_member(uuid) → boolean + // Used by: documents (project-scoped), chats (project-scoped), document_versions + // (transitively), document_edits (transitively), tabular_cells (project-scoped), + // chat_messages (transitively), is_document_member (wraps this). + // GIN-pushable: shared_with @> jsonb_build_array(lower(auth.email())) uses + // projects_shared_with_idx. + pgm.sql(` + create or replace function public.is_project_member(p_id uuid) + returns boolean + language sql + security definer + stable + set search_path = public + as $$ + select exists ( + select 1 + from public.projects + where id = p_id + and ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + ) + ); + $$; + `); + pgm.sql(`revoke all on function public.is_project_member(uuid) from public;`); + pgm.sql(`grant execute on function public.is_project_member(uuid) to authenticated, service_role;`); + + // -- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + // Helper 2: is_review_member(uuid) → boolean + // Used by: tabular_reviews SELECT, tabular_cells (when project_id IS NULL — + // direct-share path), tabular_review_chats (out of ROADMAP scope but parallel). + // Two paths: owner OR direct-share via tabular_reviews.shared_with OR + // (when project_id is set) project membership via is_project_member. + pgm.sql(` + create or replace function public.is_review_member(r_id uuid) + returns boolean + language sql + security definer + stable + set search_path = public + as $$ + select exists ( + select 1 + from public.tabular_reviews tr + where tr.id = r_id + and ( + tr.user_id = auth.uid() + or tr.shared_with @> jsonb_build_array(lower(auth.email())) + or (tr.project_id is not null and public.is_project_member(tr.project_id)) + ) + ); + $$; + `); + pgm.sql(`revoke all on function public.is_review_member(uuid) from public;`); + pgm.sql(`grant execute on function public.is_review_member(uuid) to authenticated, service_role;`); + + // -- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + // Helper 3: is_workflow_visible(uuid) → boolean + // Used by: workflows SELECT. + // The only helper that consults a separate join table (workflow_shares) rather + // than a JSONB column. + pgm.sql(` + create or replace function public.is_workflow_visible(w_id uuid) + returns boolean + language sql + security definer + stable + set search_path = public + as $$ + select exists ( + select 1 + from public.workflows w + where w.id = w_id + and w.user_id = auth.uid() + ) + or exists ( + select 1 + from public.workflow_shares ws + where ws.workflow_id = w_id + and lower(ws.shared_with_email) = lower(auth.email()) + ); + $$; + `); + pgm.sql(`revoke all on function public.is_workflow_visible(uuid) from public;`); + pgm.sql(`grant execute on function public.is_workflow_visible(uuid) to authenticated, service_role;`); + + // -- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + // Helper 4: is_chat_owner(uuid) → boolean + // Used by: chat_messages. + // Chat sharing is project-scoped, not direct. If the chat has a project_id, + // project membership grants access; otherwise owner-only. + pgm.sql(` + create or replace function public.is_chat_owner(c_id uuid) + returns boolean + language sql + security definer + stable + set search_path = public + as $$ + select exists ( + select 1 + from public.chats c + where c.id = c_id + and ( + c.user_id = auth.uid() + or (c.project_id is not null and public.is_project_member(c.project_id)) + ) + ); + $$; + `); + pgm.sql(`revoke all on function public.is_chat_owner(uuid) from public;`); + pgm.sql(`grant execute on function public.is_chat_owner(uuid) to authenticated, service_role;`); + + // -- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + // Helper 5: is_document_member(uuid) → boolean + // Used by: document_versions, document_edits. + // Wraps is_project_member against documents.project_id; handles the orphan + // case (project_id IS NULL → owner-only). + pgm.sql(` + create or replace function public.is_document_member(d_id uuid) + returns boolean + language sql + security definer + stable + set search_path = public + as $$ + select exists ( + select 1 + from public.documents d + where d.id = d_id + and ( + d.user_id = auth.uid() + or (d.project_id is not null and public.is_project_member(d.project_id)) + ) + ); + $$; + `); + pgm.sql(`revoke all on function public.is_document_member(uuid) from public;`); + pgm.sql(`grant execute on function public.is_document_member(uuid) to authenticated, service_role;`); + + // ── SECTION 2: Parallel jsonb_path_ops GIN indexes ──────────────────────── + // Adds a faster containment-only index alongside the existing jsonb_ops indexes. + // Planner picks the cheaper of the two; existing jsonb_ops indexes are preserved. + // See: RESEARCH.md §5. + pgm.sql(` + create index if not exists projects_shared_with_pathops_idx + on public.projects using gin (shared_with jsonb_path_ops); + `); + pgm.sql(` + create index if not exists tabular_reviews_shared_with_pathops_idx + on public.tabular_reviews using gin (shared_with jsonb_path_ops); + `); + + // ── SECTION 3: Enable RLS on all 14 user-owned tables ───────────────────── + // Closed-by-default: no policy = no anon access. See: RESEARCH.md Pattern 2. + // Includes tabular_review_chats* per RESEARCH.md §10 RESOLVED. + // Per D-04: the existing user-profile RLS block is preserved — only the 14 tables below. + pgm.sql(`alter table public.projects enable row level security;`); + pgm.sql(`alter table public.project_subfolders enable row level security;`); + pgm.sql(`alter table public.documents enable row level security;`); + pgm.sql(`alter table public.document_versions enable row level security;`); + pgm.sql(`alter table public.document_edits enable row level security;`); + pgm.sql(`alter table public.chats enable row level security;`); + pgm.sql(`alter table public.chat_messages enable row level security;`); + pgm.sql(`alter table public.tabular_reviews enable row level security;`); + pgm.sql(`alter table public.tabular_cells enable row level security;`); + pgm.sql(`alter table public.tabular_review_chats enable row level security;`); + pgm.sql(`alter table public.tabular_review_chat_messages enable row level security;`); + pgm.sql(`alter table public.workflows enable row level security;`); + pgm.sql(`alter table public.workflow_shares enable row level security;`); + pgm.sql(`alter table public.hidden_workflows enable row level security;`); + + // ── SECTION 4: Per-table policies ───────────────────────────────────────── + // Per D-03: mutation policies are owner-only on top-level tables. + // Child tables (document_versions, document_edits, tabular_cells, chat_messages, + // tabular_review_chat_messages) get NO mutation policy — closed-by-default service-role only. + + // ── 4.1: projects (4 policies) ────────────────────────────────────────── + pgm.sql(` + create policy "projects_select_owner_or_shared" + on public.projects for select + to authenticated + using ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + ); + `); + pgm.sql(` + create policy "projects_insert_owner" + on public.projects for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "projects_update_owner" + on public.projects for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "projects_delete_owner" + on public.projects for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.2: project_subfolders (4 policies) ────────────────────────────── + pgm.sql(` + create policy "project_subfolders_select_member" + on public.project_subfolders for select + to authenticated + using (public.is_project_member(project_id)); + `); + pgm.sql(` + create policy "project_subfolders_insert_owner" + on public.project_subfolders for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "project_subfolders_update_owner" + on public.project_subfolders for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "project_subfolders_delete_owner" + on public.project_subfolders for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.3: documents (4 policies) ──────────────────────────────────────── + pgm.sql(` + create policy "documents_select_member" + on public.documents for select + to authenticated + using ( + user_id = auth.uid() + or (project_id is not null and public.is_project_member(project_id)) + ); + `); + pgm.sql(` + create policy "documents_insert_owner" + on public.documents for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "documents_update_owner" + on public.documents for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "documents_delete_owner" + on public.documents for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.4: document_versions (1 policy — SELECT only; no user_id column) ── + // Mutations are service-role only — no INSERT/UPDATE/DELETE policy means + // closed-by-default (anon-key cannot write). Service role bypasses RLS. + pgm.sql(` + create policy "document_versions_select_member" + on public.document_versions for select + to authenticated + using (public.is_document_member(document_id)); + `); + + // ── 4.5: document_edits (1 policy — SELECT only; no user_id column) ──── + // No INSERT/UPDATE/DELETE policy — service-role only. (Anon-key clients in + // v1 do not need to mutate edits; UI accept/reject calls go through backend.) + pgm.sql(` + create policy "document_edits_select_member" + on public.document_edits for select + to authenticated + using (public.is_document_member(document_id)); + `); + + // ── 4.6: chats (4 policies) ───────────────────────────────────────────── + pgm.sql(` + create policy "chats_select_owner_or_project_member" + on public.chats for select + to authenticated + using ( + user_id = auth.uid() + or (project_id is not null and public.is_project_member(project_id)) + ); + `); + pgm.sql(` + create policy "chats_insert_owner" + on public.chats for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "chats_update_owner" + on public.chats for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "chats_delete_owner" + on public.chats for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.7: chat_messages (1 policy — SELECT only; no user_id column) ───── + // No mutation policies — service-role-only writes (chat_messages are written + // by the SSE stream loop, never by the anon client). + pgm.sql(` + create policy "chat_messages_select_chat_member" + on public.chat_messages for select + to authenticated + using (public.is_chat_owner(chat_id)); + `); + + // ── 4.8: tabular_reviews (4 policies) ────────────────────────────────── + pgm.sql(` + create policy "tabular_reviews_select_member" + on public.tabular_reviews for select + to authenticated + using ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + or (project_id is not null and public.is_project_member(project_id)) + ); + `); + pgm.sql(` + create policy "tabular_reviews_insert_owner" + on public.tabular_reviews for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "tabular_reviews_update_owner" + on public.tabular_reviews for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "tabular_reviews_delete_owner" + on public.tabular_reviews for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.9: tabular_cells (1 policy — SELECT only; no user_id column) ───── + // tabular_cells has no user_id; gated through is_review_member (which itself + // handles project_id IS NULL direct-share path). + // No mutation policies — service-role only. + pgm.sql(` + create policy "tabular_cells_select_review_member" + on public.tabular_cells for select + to authenticated + using (public.is_review_member(review_id)); + `); + + // ── 4.10: tabular_review_chats (4 policies) ───────────────────────────── + // Per RESEARCH.md §10 RESOLVED: mirrors chats/chat_messages shape. + // Owner-only mutations; member SELECT via review membership. + pgm.sql(` + create policy "tabular_review_chats_select_owner_or_review_member" + on public.tabular_review_chats for select + to authenticated + using (auth.uid() = user_id or public.is_review_member(review_id)); + `); + pgm.sql(` + create policy "tabular_review_chats_insert_owner" + on public.tabular_review_chats for insert + to authenticated + with check (auth.uid() = user_id); + `); + pgm.sql(` + create policy "tabular_review_chats_update_owner" + on public.tabular_review_chats for update + to authenticated + using (auth.uid() = user_id) + with check (auth.uid() = user_id); + `); + pgm.sql(` + create policy "tabular_review_chats_delete_owner" + on public.tabular_review_chats for delete + to authenticated + using (auth.uid() = user_id); + `); + + // ── 4.11: tabular_review_chat_messages (1 policy — SELECT only; no user_id) ─ + // Per RESEARCH.md §10 RESOLVED: mirrors chat_messages shape. + // No mutation policies — service-role only; tabular_review_chat_messages has + // no user_id column to anchor mutations on. + pgm.sql(` + create policy "tabular_review_chat_messages_select_chat_member" + on public.tabular_review_chat_messages for select + to authenticated + using ( + exists ( + select 1 from public.tabular_review_chats c + where c.id = chat_id + and (c.user_id = auth.uid() or public.is_review_member(c.review_id)) + ) + ); + `); + + // ── 4.12: workflows (4 policies) ─────────────────────────────────────── + // workflows.user_id is NULLABLE (built-in workflows have user_id = NULL). + // The SELECT policy must allow built-ins to be read by all authenticated users. + pgm.sql(` + create policy "workflows_select_visible_or_builtin" + on public.workflows for select + to authenticated + using ( + is_system = true + or user_id = auth.uid() + or public.is_workflow_visible(id) + ); + `); + pgm.sql(` + create policy "workflows_insert_owner" + on public.workflows for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "workflows_update_owner" + on public.workflows for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "workflows_delete_owner" + on public.workflows for delete + to authenticated + using (user_id = auth.uid()); + `); + + // ── 4.13: workflow_shares (4 policies) — D-05 ───────────────────────── + // SELECT visible to owner AND recipient; mutations owner-only. + pgm.sql(` + create policy "workflow_shares_select_owner_or_recipient" + on public.workflow_shares for select + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_shares.workflow_id + and w.user_id = auth.uid() + ) + or lower(shared_with_email) = lower(auth.email()) + ); + `); + pgm.sql(` + create policy "workflow_shares_insert_workflow_owner" + on public.workflow_shares for insert + to authenticated + with check ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + `); + pgm.sql(` + create policy "workflow_shares_update_workflow_owner" + on public.workflow_shares for update + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + `); + pgm.sql(` + create policy "workflow_shares_delete_workflow_owner" + on public.workflow_shares for delete + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + `); + + // ── 4.14: hidden_workflows (4 policies — owner-only) ─────────────────── + pgm.sql(` + create policy "hidden_workflows_select_owner" + on public.hidden_workflows for select + to authenticated + using (user_id = auth.uid()); + `); + pgm.sql(` + create policy "hidden_workflows_insert_owner" + on public.hidden_workflows for insert + to authenticated + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "hidden_workflows_update_owner" + on public.hidden_workflows for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + `); + pgm.sql(` + create policy "hidden_workflows_delete_owner" + on public.hidden_workflows for delete + to authenticated + using (user_id = auth.uid()); + `); +} + +export async function down(pgm: MigrationBuilder): Promise { + // Drop all policies in reverse order. + const policies: Array<[string, string]> = [ + // hidden_workflows (4) + ["hidden_workflows_delete_owner", "hidden_workflows"], + ["hidden_workflows_update_owner", "hidden_workflows"], + ["hidden_workflows_insert_owner", "hidden_workflows"], + ["hidden_workflows_select_owner", "hidden_workflows"], + // workflow_shares (4) + ["workflow_shares_delete_workflow_owner", "workflow_shares"], + ["workflow_shares_update_workflow_owner", "workflow_shares"], + ["workflow_shares_insert_workflow_owner", "workflow_shares"], + ["workflow_shares_select_owner_or_recipient", "workflow_shares"], + // workflows (4) + ["workflows_delete_owner", "workflows"], + ["workflows_update_owner", "workflows"], + ["workflows_insert_owner", "workflows"], + ["workflows_select_visible_or_builtin", "workflows"], + // tabular_review_chat_messages (1) + ["tabular_review_chat_messages_select_chat_member", "tabular_review_chat_messages"], + // tabular_review_chats (4) + ["tabular_review_chats_delete_owner", "tabular_review_chats"], + ["tabular_review_chats_update_owner", "tabular_review_chats"], + ["tabular_review_chats_insert_owner", "tabular_review_chats"], + ["tabular_review_chats_select_owner_or_review_member", "tabular_review_chats"], + // tabular_cells (1) + ["tabular_cells_select_review_member", "tabular_cells"], + // tabular_reviews (4) + ["tabular_reviews_delete_owner", "tabular_reviews"], + ["tabular_reviews_update_owner", "tabular_reviews"], + ["tabular_reviews_insert_owner", "tabular_reviews"], + ["tabular_reviews_select_member", "tabular_reviews"], + // chat_messages (1) + ["chat_messages_select_chat_member", "chat_messages"], + // chats (4) + ["chats_delete_owner", "chats"], + ["chats_update_owner", "chats"], + ["chats_insert_owner", "chats"], + ["chats_select_owner_or_project_member", "chats"], + // document_edits (1) + ["document_edits_select_member", "document_edits"], + // document_versions (1) + ["document_versions_select_member", "document_versions"], + // documents (4) + ["documents_delete_owner", "documents"], + ["documents_update_owner", "documents"], + ["documents_insert_owner", "documents"], + ["documents_select_member", "documents"], + // project_subfolders (4) + ["project_subfolders_delete_owner", "project_subfolders"], + ["project_subfolders_update_owner", "project_subfolders"], + ["project_subfolders_insert_owner", "project_subfolders"], + ["project_subfolders_select_member", "project_subfolders"], + // projects (4) + ["projects_delete_owner", "projects"], + ["projects_update_owner", "projects"], + ["projects_insert_owner", "projects"], + ["projects_select_owner_or_shared", "projects"], + ]; + for (const [name, table] of policies) { + pgm.sql(`drop policy if exists "${name}" on public.${table};`); + } + + // Disable RLS — same 14 tables as up, in reverse order. + pgm.sql(`alter table public.hidden_workflows disable row level security;`); + pgm.sql(`alter table public.workflow_shares disable row level security;`); + pgm.sql(`alter table public.workflows disable row level security;`); + pgm.sql(`alter table public.tabular_review_chat_messages disable row level security;`); + pgm.sql(`alter table public.tabular_review_chats disable row level security;`); + pgm.sql(`alter table public.tabular_cells disable row level security;`); + pgm.sql(`alter table public.tabular_reviews disable row level security;`); + pgm.sql(`alter table public.chat_messages disable row level security;`); + pgm.sql(`alter table public.chats disable row level security;`); + pgm.sql(`alter table public.document_edits disable row level security;`); + pgm.sql(`alter table public.document_versions disable row level security;`); + pgm.sql(`alter table public.documents disable row level security;`); + pgm.sql(`alter table public.project_subfolders disable row level security;`); + pgm.sql(`alter table public.projects disable row level security;`) + + // Drop the parallel jsonb_path_ops indexes. + pgm.sql(`drop index if exists public.projects_shared_with_pathops_idx;`); + pgm.sql(`drop index if exists public.tabular_reviews_shared_with_pathops_idx;`); + + // Drop helpers in reverse dependency order. + pgm.sql(`drop function if exists public.is_document_member(uuid);`); + pgm.sql(`drop function if exists public.is_chat_owner(uuid);`); + pgm.sql(`drop function if exists public.is_workflow_visible(uuid);`); + pgm.sql(`drop function if exists public.is_review_member(uuid);`); + pgm.sql(`drop function if exists public.is_project_member(uuid);`); +} diff --git a/backend/migrations/0006_workflow_shares_with_check.ts b/backend/migrations/0006_workflow_shares_with_check.ts new file mode 100644 index 000000000..354ee353e --- /dev/null +++ b/backend/migrations/0006_workflow_shares_with_check.ts @@ -0,0 +1,72 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +// CR-03 (Phase 11 verification gap): workflow_shares UPDATE policy was missing WITH CHECK. +// +// The policy created in 0005_rls_policies.ts:494-506 has only a USING clause. PostgreSQL +// defaults WITH CHECK to the USING expression when WITH CHECK is omitted, which protects +// against re-pointing workflow_id to a workflow the caller does not own — but leaves +// shared_by_user_id unconstrained. A workflow owner can mutate shared_by_user_id on their +// own share row to a foreign UUID, forging the audit trail of who shared the workflow. +// +// Fix (additive — does NOT edit 0005 in place): +// 1. Drop the buggy USING-only policy. +// 2. Recreate with both USING and an explicit WITH CHECK that matches the INSERT policy +// (workflows.user_id = auth.uid() — owner-only). +// +// The same SQL is mirrored into backend/migrations/000_one_shot_schema.sql so fresh +// installs land at the corrected state. + +export const shorthands = undefined; + +export async function up(pgm: MigrationBuilder): Promise { + // Drop the buggy USING-only policy created by 0005. + pgm.sql(`drop policy if exists "workflow_shares_update_workflow_owner" on public.workflow_shares;`); + + // Recreate with explicit WITH CHECK that pins BOTH workflow_id and shared_by_user_id. + // USING gates the pre-update row (caller must own the workflow). WITH CHECK gates the + // post-update row: (a) the updated workflow_id must still point to a workflow the + // caller owns, AND (b) shared_by_user_id must remain the caller's own uid so the + // audit trail of who shared the workflow cannot be re-attributed to a foreign user. + pgm.sql(` + create policy "workflow_shares_update_workflow_owner" + on public.workflow_shares for update + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ) + with check ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + and shared_by_user_id = auth.uid() + ); + `); +} + +export async function down(pgm: MigrationBuilder): Promise { + // Reverse to the (buggy) USING-only state created by 0005 so the migration is reversible. + // This is intentional — the down() must restore the prior state, not "improve" it. + pgm.sql(`drop policy if exists "workflow_shares_update_workflow_owner" on public.workflow_shares;`); + + pgm.sql(` + create policy "workflow_shares_update_workflow_owner" + on public.workflow_shares for update + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + `); +} diff --git a/backend/migrations/0007_encrypt_api_keys.ts b/backend/migrations/0007_encrypt_api_keys.ts new file mode 100644 index 000000000..d52cba453 --- /dev/null +++ b/backend/migrations/0007_encrypt_api_keys.ts @@ -0,0 +1,31 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +/** + * CLEAN-05 — envelope-encrypt LLM API keys at rest. + * Drops plaintext columns and adds six bytea columns (ciphertext/iv/auth_tag per provider). + * + * Down migration cannot restore plaintext data (acceptable per + * .planning/phases/12-encrypted-keys-account-deletion-cascade/12-CONTEXT.md + * — pre-launch, no production users). + */ +export async function up(pgm: MigrationBuilder): Promise { + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN claude_api_key_ciphertext bytea"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN claude_api_key_iv bytea"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN claude_api_key_auth_tag bytea"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN gemini_api_key_ciphertext bytea"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN gemini_api_key_iv bytea"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN gemini_api_key_auth_tag bytea"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS claude_api_key"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS gemini_api_key"); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS claude_api_key text"); + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN IF NOT EXISTS gemini_api_key text"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS claude_api_key_ciphertext"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS claude_api_key_iv"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS claude_api_key_auth_tag"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS gemini_api_key_ciphertext"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS gemini_api_key_iv"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS gemini_api_key_auth_tag"); +} diff --git a/backend/migrations/0008_soft_delete_user_profiles.ts b/backend/migrations/0008_soft_delete_user_profiles.ts new file mode 100644 index 000000000..d09e98589 --- /dev/null +++ b/backend/migrations/0008_soft_delete_user_profiles.ts @@ -0,0 +1,16 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +/** + * CLEAN-44 — 30-day soft-delete grace window for account deletion. + * Adds user_profiles.deleted_at; partial index supports the requireAuth gate + * without slowing inserts/updates on active users. + */ +export async function up(pgm: MigrationBuilder): Promise { + pgm.sql("ALTER TABLE public.user_profiles ADD COLUMN deleted_at timestamptz"); + pgm.sql("CREATE INDEX idx_user_profiles_deleted_at ON public.user_profiles(user_id) WHERE deleted_at IS NOT NULL"); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.sql("DROP INDEX IF EXISTS public.idx_user_profiles_deleted_at"); + pgm.sql("ALTER TABLE public.user_profiles DROP COLUMN IF EXISTS deleted_at"); +} diff --git a/backend/migrations/0009_account_deletion_jobs.ts b/backend/migrations/0009_account_deletion_jobs.ts new file mode 100644 index 000000000..27687cd5f --- /dev/null +++ b/backend/migrations/0009_account_deletion_jobs.ts @@ -0,0 +1,29 @@ +import type { MigrationBuilder } from "node-pg-migrate"; + +/** + * CLEAN-44 — persistent state for the 30-day delayed account-deletion worker. + * One row per soft-deleted user; the worker polls scheduled_for <= now() WHERE status='pending'. + * FK CASCADE means the row vanishes once admin.deleteUser fires — operator log is the audit trail. + */ +export async function up(pgm: MigrationBuilder): Promise { + pgm.sql(` + CREATE TABLE public.account_deletion_jobs ( + user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + scheduled_for timestamptz NOT NULL, + status text NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'running', 'done', 'failed', 'cancelled')), + last_continuation_token jsonb, + restore_token_used_at timestamptz, + claimed_by text, + claimed_at timestamptz, + attempts int NOT NULL DEFAULT 0, + last_error text, + created_at timestamptz NOT NULL DEFAULT now() + ) + `); + pgm.sql("CREATE INDEX idx_account_deletion_jobs_due ON public.account_deletion_jobs(scheduled_for) WHERE status = 'pending'"); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.sql("DROP TABLE IF EXISTS public.account_deletion_jobs"); +} diff --git a/backend/migrations/000_one_shot_schema.sql b/backend/migrations/000_one_shot_schema.sql new file mode 100644 index 000000000..0d2462589 --- /dev/null +++ b/backend/migrations/000_one_shot_schema.sql @@ -0,0 +1,898 @@ +-- Mike one-shot Supabase schema +-- Based on supabase-migration.sql plus the later backend/migrations/*.sql files. +-- Use this for a fresh Supabase database. Existing deployments should continue +-- to apply the incremental migration files instead. + +create extension if not exists "pgcrypto"; + +-- --------------------------------------------------------------------------- +-- User profiles +-- --------------------------------------------------------------------------- + +create table if not exists public.user_profiles ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null unique references auth.users(id) on delete cascade, + display_name text, + organisation text, + tabular_model text not null default 'gemini-3-flash-preview', + -- Phase 12: encrypted API keys + soft-delete + claude_api_key_ciphertext bytea, + claude_api_key_iv bytea, + claude_api_key_auth_tag bytea, + gemini_api_key_ciphertext bytea, + gemini_api_key_iv bytea, + gemini_api_key_auth_tag bytea, + deleted_at timestamptz, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_user_profiles_user + on public.user_profiles(user_id); + +-- Phase 12: partial index for soft-delete auth gate (CLEAN-44) +create index if not exists idx_user_profiles_deleted_at + on public.user_profiles(user_id) where deleted_at is not null; + +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 +language plpgsql +security definer +set search_path = public +as $$ +begin + insert into public.user_profiles (user_id) + values (new.id) + on conflict (user_id) do nothing; + return new; +exception when others then + -- Never block signup if the profile insert fails. + return new; +end; +$$; + +drop trigger if exists on_auth_user_created on auth.users; +create trigger on_auth_user_created + after insert on auth.users + for each row execute procedure public.handle_new_user(); + +-- --------------------------------------------------------------------------- +-- Projects and documents +-- --------------------------------------------------------------------------- + +create table if not exists public.projects ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users(id) on delete cascade, + name text not null, + cm_number text, + visibility text not null default 'private', + shared_with jsonb not null default '[]'::jsonb, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_projects_user + on public.projects(user_id); + +create index if not exists projects_shared_with_idx + on public.projects using gin (shared_with); + +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, + user_id uuid not null references auth.users(id) on delete cascade, + name text not null, + parent_folder_id uuid references public.project_subfolders(id) on delete cascade, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_project_subfolders_project + on public.project_subfolders(project_id); + +create table if not exists public.documents ( + id uuid primary key default gen_random_uuid(), + project_id uuid references public.projects(id) on delete cascade, + user_id uuid not null references auth.users(id) on delete cascade, + filename text not null, + file_type text, + size_bytes integer not null default 0, + page_count integer, + structure_tree jsonb, + status text not null default 'pending', + pdf_conversion_status text not null default 'ok' check (pdf_conversion_status in ('pending', 'ok', 'failed')), + folder_id uuid references public.project_subfolders(id) on delete set null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_documents_user_project + on public.documents(user_id, project_id); + +create index if not exists idx_documents_project_folder + on public.documents(project_id, folder_id); + +create table if not exists public.document_versions ( + id uuid primary key default gen_random_uuid(), + document_id uuid not null references public.documents(id) on delete cascade, + storage_path text not null, + pdf_storage_path text, + source text not null default 'upload', + version_number integer, + display_name text, + created_at timestamptz not null default now(), + constraint document_versions_source_check + check (source = any (array[ + 'upload'::text, + 'user_upload'::text, + 'assistant_edit'::text, + 'user_accept'::text, + 'user_reject'::text, + 'generated'::text + ])) +); + +create index if not exists document_versions_document_id_idx + on public.document_versions(document_id, created_at desc); + +create index if not exists document_versions_doc_vnum_idx + on public.document_versions(document_id, version_number); + +alter table public.document_versions + add constraint document_versions_doc_version_unique unique (document_id, version_number); + +alter table public.documents + add column if not exists current_version_id uuid + references public.document_versions(id) on delete set null; + +create table if not exists public.document_edits ( + id uuid primary key default gen_random_uuid(), + document_id uuid not null references public.documents(id) on delete cascade, + chat_message_id uuid, + version_id uuid not null references public.document_versions(id) on delete cascade, + change_id text not null, + del_w_id text, + ins_w_id text, + deleted_text text not null default '', + inserted_text text not null default '', + context_before text, + context_after text, + status text not null default 'pending' + check (status = any (array[ + 'pending'::text, + 'accepted'::text, + 'rejected'::text + ])), + created_at timestamptz not null default now(), + resolved_at timestamptz +); + +create index if not exists document_edits_document_id_idx + on public.document_edits(document_id, created_at desc); + +create index if not exists document_edits_message_id_idx + on public.document_edits(chat_message_id); + +create index if not exists document_edits_version_id_idx + on public.document_edits(version_id); + +-- --------------------------------------------------------------------------- +-- Workflows +-- --------------------------------------------------------------------------- + +create table if not exists public.workflows ( + id uuid primary key default gen_random_uuid(), + user_id uuid references auth.users(id) on delete cascade, + title text not null, + type text not null, + prompt_md text, + columns_config jsonb, + practice text, + is_system boolean not null default false, + created_at timestamptz not null default now() +); + +create index if not exists idx_workflows_user + on public.workflows(user_id); + +create table if not exists public.hidden_workflows ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users(id) on delete cascade, + workflow_id text not null, + created_at timestamptz not null default now(), + unique(user_id, workflow_id) +); + +create index if not exists idx_hidden_workflows_user + on public.hidden_workflows(user_id); + +create table if not exists public.workflow_shares ( + id uuid primary key default gen_random_uuid(), + workflow_id uuid not null references public.workflows(id) on delete cascade, + shared_by_user_id uuid not null references auth.users(id) on delete cascade, + shared_with_email text not null, + allow_edit boolean not null default false, + created_at timestamptz not null default now(), + constraint workflow_shares_workflow_email_unique + unique(workflow_id, shared_with_email) +); + +create index if not exists workflow_shares_workflow_id_idx + on public.workflow_shares(workflow_id); + +create index if not exists workflow_shares_email_idx + on public.workflow_shares(shared_with_email); + +-- --------------------------------------------------------------------------- +-- Assistant chats +-- --------------------------------------------------------------------------- + +create table if not exists public.chats ( + id uuid primary key default gen_random_uuid(), + project_id uuid references public.projects(id) on delete cascade, + user_id uuid not null references auth.users(id) on delete cascade, + title text, + created_at timestamptz not null default now() +); + +create index if not exists idx_chats_user + on public.chats(user_id); + +create index if not exists idx_chats_project + on public.chats(project_id); + +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, + role text not null, + content jsonb, + files jsonb, + annotations jsonb, + created_at timestamptz not null default now() +); + +create index if not exists idx_chat_messages_chat + on public.chat_messages(chat_id); + +do $$ +begin + if not exists ( + select 1 + from pg_constraint + where conname = 'document_edits_chat_message_id_fkey' + and conrelid = 'public.document_edits'::regclass + ) then + alter table public.document_edits + add constraint document_edits_chat_message_id_fkey + foreign key (chat_message_id) + references public.chat_messages(id) + on delete set null; + end if; +end; +$$; + +-- --------------------------------------------------------------------------- +-- Tabular reviews +-- --------------------------------------------------------------------------- + +create table if not exists public.tabular_reviews ( + id uuid primary key default gen_random_uuid(), + project_id uuid references public.projects(id) on delete cascade, + user_id uuid not null references auth.users(id) on delete cascade, + title text, + columns_config jsonb, + workflow_id uuid references public.workflows(id) on delete set null, + practice text, + shared_with jsonb not null default '[]'::jsonb, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_tabular_reviews_user + on public.tabular_reviews(user_id); + +create index if not exists idx_tabular_reviews_project + on public.tabular_reviews(project_id); + +create index if not exists tabular_reviews_shared_with_idx + on public.tabular_reviews using gin (shared_with); + +create table if not exists public.tabular_cells ( + id uuid primary key default gen_random_uuid(), + review_id uuid not null references public.tabular_reviews(id) on delete cascade, + document_id uuid not null references public.documents(id) on delete cascade, + column_index integer not null, + content text, + citations jsonb, + status text not null default 'pending', + created_at timestamptz not null default now() +); + +create index if not exists idx_tabular_cells_review + on public.tabular_cells(review_id, document_id, column_index); + +create table if not exists public.tabular_review_chats ( + id uuid primary key default gen_random_uuid(), + review_id uuid not null references public.tabular_reviews(id) on delete cascade, + user_id uuid not null references auth.users(id) on delete cascade, + title text, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists tabular_review_chats_review_idx + on public.tabular_review_chats(review_id, updated_at desc); + +create index if not exists tabular_review_chats_user_idx + on public.tabular_review_chats(user_id); + +create table if not exists public.tabular_review_chat_messages ( + id uuid primary key default gen_random_uuid(), + chat_id uuid not null references public.tabular_review_chats(id) on delete cascade, + role text not null, + content jsonb, + annotations jsonb, + created_at timestamptz not null default now() +); + +create index if not exists tabular_review_chat_messages_chat_idx + on public.tabular_review_chat_messages(chat_id, created_at); + +-- --------------------------------------------------------------------------- +-- RPCs — server-side aggregations (CLEAN-28) +-- --------------------------------------------------------------------------- + +-- Returns distinct document counts per tabular review. +-- EXPLAIN confirms idx_tabular_cells_review (review_id, document_id, column_index) +-- covers the (review_id, document_id) prefix via leftmost prefix — no new index needed. +create or replace function public.select_review_doc_counts(review_ids uuid[]) +returns table (review_id uuid, doc_count bigint) +language sql +stable +security definer +set search_path = '' +as $$ + select review_id, count(distinct document_id) as doc_count + from public.tabular_cells + where review_id = any(review_ids) + group by review_id; +$$; + +revoke all on function public.select_review_doc_counts(uuid[]) from public, anon, authenticated; +grant execute on function public.select_review_doc_counts(uuid[]) to service_role; + +-- CLEAN-29: document shared_with JSONB canonical shape on the two tables that carry it. +comment on column public.projects.shared_with is + 'Canonical shape: to_jsonb(array[lower(email)]) — array of lowercased email strings. Normalized at write in routes/projects.ts:265-275. Consolidation to a resource_shares table deferred to v2 (Phase 10 / CLEAN-29).'; + +comment on column public.tabular_reviews.shared_with is + 'Canonical shape: to_jsonb(array[lower(email)]) — array of lowercased email strings. Normalized at write in routes/tabular.ts. Consolidation to a resource_shares table deferred to v2 (Phase 10 / CLEAN-29).'; + +-- --------------------------------------------------------------------------- +-- Phase 12: encrypted API keys + soft-delete + deletion jobs (CLEAN-05, CLEAN-44) +-- (mirrors backend/migrations/0009_account_deletion_jobs.ts) +-- --------------------------------------------------------------------------- + +create table if not exists public.account_deletion_jobs ( + user_id uuid primary key references auth.users(id) on delete cascade, + scheduled_for timestamptz not null, + status text not null default 'pending' + check (status in ('pending', 'running', 'done', 'failed', 'cancelled')), + last_continuation_token jsonb, + restore_token_used_at timestamptz, + claimed_by text, + claimed_at timestamptz, + attempts int not null default 0, + last_error text, + created_at timestamptz not null default now() +); + +create index if not exists idx_account_deletion_jobs_due + on public.account_deletion_jobs(scheduled_for) where status = 'pending'; + +-- --------------------------------------------------------------------------- +-- CLEAN-47: RLS defense-in-depth (mirrors backend/migrations/0005_rls_policies.ts) +-- See .planning/phases/11-rls-defense-in-depth/11-RESEARCH.md §2-§3 + §10 RESOLVED. +-- --------------------------------------------------------------------------- + +-- ── Helper functions ──────────────────────────────────────────────────────── +-- All helpers: language sql security definer stable set search_path = public. +-- DO NOT change to plpgsql — breaks GIN-pushdown (RESEARCH §2 Pitfall 1). + +-- Helper 1: is_project_member(uuid) → boolean +-- Used by: documents, chats, document_versions, document_edits, tabular_cells, +-- chat_messages (transitively), is_document_member (wraps this). +create or replace function public.is_project_member(p_id uuid) +returns boolean +language sql +security definer +stable +set search_path = public +as $$ + select exists ( + select 1 + from public.projects + where id = p_id + and ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + ) + ); +$$; + +revoke all on function public.is_project_member(uuid) from public; +grant execute on function public.is_project_member(uuid) to authenticated, service_role; + +-- Helper 2: is_review_member(uuid) → boolean +-- Used by: tabular_reviews SELECT, tabular_cells, tabular_review_chats. +create or replace function public.is_review_member(r_id uuid) +returns boolean +language sql +security definer +stable +set search_path = public +as $$ + select exists ( + select 1 + from public.tabular_reviews tr + where tr.id = r_id + and ( + tr.user_id = auth.uid() + or tr.shared_with @> jsonb_build_array(lower(auth.email())) + or (tr.project_id is not null and public.is_project_member(tr.project_id)) + ) + ); +$$; + +revoke all on function public.is_review_member(uuid) from public; +grant execute on function public.is_review_member(uuid) to authenticated, service_role; + +-- Helper 3: is_workflow_visible(uuid) → boolean +-- Used by: workflows SELECT. +create or replace function public.is_workflow_visible(w_id uuid) +returns boolean +language sql +security definer +stable +set search_path = public +as $$ + select exists ( + select 1 + from public.workflows w + where w.id = w_id + and w.user_id = auth.uid() + ) + or exists ( + select 1 + from public.workflow_shares ws + where ws.workflow_id = w_id + and lower(ws.shared_with_email) = lower(auth.email()) + ); +$$; + +revoke all on function public.is_workflow_visible(uuid) from public; +grant execute on function public.is_workflow_visible(uuid) to authenticated, service_role; + +-- Helper 4: is_chat_owner(uuid) → boolean +-- Used by: chat_messages. +create or replace function public.is_chat_owner(c_id uuid) +returns boolean +language sql +security definer +stable +set search_path = public +as $$ + select exists ( + select 1 + from public.chats c + where c.id = c_id + and ( + c.user_id = auth.uid() + or (c.project_id is not null and public.is_project_member(c.project_id)) + ) + ); +$$; + +revoke all on function public.is_chat_owner(uuid) from public; +grant execute on function public.is_chat_owner(uuid) to authenticated, service_role; + +-- Helper 5: is_document_member(uuid) → boolean +-- Used by: document_versions, document_edits. +create or replace function public.is_document_member(d_id uuid) +returns boolean +language sql +security definer +stable +set search_path = public +as $$ + select exists ( + select 1 + from public.documents d + where d.id = d_id + and ( + d.user_id = auth.uid() + or (d.project_id is not null and public.is_project_member(d.project_id)) + ) + ); +$$; + +revoke all on function public.is_document_member(uuid) from public; +grant execute on function public.is_document_member(uuid) to authenticated, service_role; + +-- ── Parallel jsonb_path_ops GIN indexes ──────────────────────────────────── +-- Adds faster containment-only index; planner picks cheaper of the two. +-- Existing jsonb_ops indexes (projects_shared_with_idx, tabular_reviews_shared_with_idx) +-- are preserved — only additive. +create index if not exists projects_shared_with_pathops_idx + on public.projects using gin (shared_with jsonb_path_ops); + +create index if not exists tabular_reviews_shared_with_pathops_idx + on public.tabular_reviews using gin (shared_with jsonb_path_ops); + +-- ── Enable RLS on 14 user-owned tables ───────────────────────────────────── +-- Closed-by-default: no policy = no anon access. +-- The existing user-profile RLS block (above) is preserved per D-04. +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.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; +alter table public.workflows enable row level security; +alter table public.workflow_shares enable row level security; +alter table public.hidden_workflows enable row level security; + +-- ── Per-table policies (idempotent drop-then-create pairs) ───────────────── + +-- projects (4 policies) +drop policy if exists "projects_select_owner_or_shared" on public.projects; +create policy "projects_select_owner_or_shared" + on public.projects for select + to authenticated + using ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + ); + +drop policy if exists "projects_insert_owner" on public.projects; +create policy "projects_insert_owner" + on public.projects for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "projects_update_owner" on public.projects; +create policy "projects_update_owner" + on public.projects for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "projects_delete_owner" on public.projects; +create policy "projects_delete_owner" + on public.projects for delete + to authenticated + using (user_id = auth.uid()); + +-- project_subfolders (4 policies) +drop policy if exists "project_subfolders_select_member" on public.project_subfolders; +create policy "project_subfolders_select_member" + on public.project_subfolders for select + to authenticated + using (public.is_project_member(project_id)); + +drop policy if exists "project_subfolders_insert_owner" on public.project_subfolders; +create policy "project_subfolders_insert_owner" + on public.project_subfolders for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "project_subfolders_update_owner" on public.project_subfolders; +create policy "project_subfolders_update_owner" + on public.project_subfolders for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "project_subfolders_delete_owner" on public.project_subfolders; +create policy "project_subfolders_delete_owner" + on public.project_subfolders for delete + to authenticated + using (user_id = auth.uid()); + +-- documents (4 policies) +drop policy if exists "documents_select_member" on public.documents; +create policy "documents_select_member" + on public.documents for select + to authenticated + using ( + user_id = auth.uid() + or (project_id is not null and public.is_project_member(project_id)) + ); + +drop policy if exists "documents_insert_owner" on public.documents; +create policy "documents_insert_owner" + on public.documents for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "documents_update_owner" on public.documents; +create policy "documents_update_owner" + on public.documents for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "documents_delete_owner" on public.documents; +create policy "documents_delete_owner" + on public.documents for delete + to authenticated + using (user_id = auth.uid()); + +-- document_versions (1 policy — SELECT only; no user_id column) +-- Mutations are service-role only. +drop policy if exists "document_versions_select_member" on public.document_versions; +create policy "document_versions_select_member" + on public.document_versions for select + to authenticated + using (public.is_document_member(document_id)); + +-- document_edits (1 policy — SELECT only; no user_id column) +-- Mutations are service-role only. +drop policy if exists "document_edits_select_member" on public.document_edits; +create policy "document_edits_select_member" + on public.document_edits for select + to authenticated + using (public.is_document_member(document_id)); + +-- chats (4 policies) +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 + to authenticated + using ( + user_id = auth.uid() + or (project_id is not null and public.is_project_member(project_id)) + ); + +drop policy if exists "chats_insert_owner" on public.chats; +create policy "chats_insert_owner" + on public.chats for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "chats_update_owner" on public.chats; +create policy "chats_update_owner" + on public.chats for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "chats_delete_owner" on public.chats; +create policy "chats_delete_owner" + on public.chats for delete + to authenticated + using (user_id = auth.uid()); + +-- chat_messages (1 policy — SELECT only; no user_id column) +-- Mutations are service-role only. +drop policy if exists "chat_messages_select_chat_member" on public.chat_messages; +create policy "chat_messages_select_chat_member" + on public.chat_messages for select + to authenticated + using (public.is_chat_owner(chat_id)); + +-- tabular_reviews (4 policies) +drop policy if exists "tabular_reviews_select_member" on public.tabular_reviews; +create policy "tabular_reviews_select_member" + on public.tabular_reviews for select + to authenticated + using ( + user_id = auth.uid() + or shared_with @> jsonb_build_array(lower(auth.email())) + or (project_id is not null and public.is_project_member(project_id)) + ); + +drop policy if exists "tabular_reviews_insert_owner" on public.tabular_reviews; +create policy "tabular_reviews_insert_owner" + on public.tabular_reviews for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "tabular_reviews_update_owner" on public.tabular_reviews; +create policy "tabular_reviews_update_owner" + on public.tabular_reviews for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "tabular_reviews_delete_owner" on public.tabular_reviews; +create policy "tabular_reviews_delete_owner" + on public.tabular_reviews for delete + to authenticated + using (user_id = auth.uid()); + +-- tabular_cells (1 policy — SELECT only; no user_id column) +-- Mutations are service-role only. +drop policy if exists "tabular_cells_select_review_member" on public.tabular_cells; +create policy "tabular_cells_select_review_member" + on public.tabular_cells for select + to authenticated + using (public.is_review_member(review_id)); + +-- tabular_review_chats (4 policies) — per RESEARCH.md §10 RESOLVED +-- Owner-only mutations; member SELECT via review membership. +drop policy if exists "tabular_review_chats_select_owner_or_review_member" on public.tabular_review_chats; +create policy "tabular_review_chats_select_owner_or_review_member" + on public.tabular_review_chats for select + to authenticated + using (auth.uid() = user_id or public.is_review_member(review_id)); + +drop policy if exists "tabular_review_chats_insert_owner" on public.tabular_review_chats; +create policy "tabular_review_chats_insert_owner" + on public.tabular_review_chats for insert + to authenticated + with check (auth.uid() = user_id); + +drop policy if exists "tabular_review_chats_update_owner" on public.tabular_review_chats; +create policy "tabular_review_chats_update_owner" + on public.tabular_review_chats for update + to authenticated + using (auth.uid() = user_id) + with check (auth.uid() = user_id); + +drop policy if exists "tabular_review_chats_delete_owner" on public.tabular_review_chats; +create policy "tabular_review_chats_delete_owner" + on public.tabular_review_chats for delete + to authenticated + using (auth.uid() = user_id); + +-- tabular_review_chat_messages (1 policy — SELECT only; no user_id column) +-- Mutations are service-role only; mirrors chat_messages pattern. +drop policy if exists "tabular_review_chat_messages_select_chat_member" on public.tabular_review_chat_messages; +create policy "tabular_review_chat_messages_select_chat_member" + on public.tabular_review_chat_messages for select + to authenticated + using ( + exists ( + select 1 from public.tabular_review_chats c + where c.id = chat_id + and (c.user_id = auth.uid() or public.is_review_member(c.review_id)) + ) + ); + +-- workflows (4 policies) +-- workflows.user_id is NULLABLE (built-in workflows have user_id = NULL). +drop policy if exists "workflows_select_visible_or_builtin" on public.workflows; +create policy "workflows_select_visible_or_builtin" + on public.workflows for select + to authenticated + using ( + is_system = true + or user_id = auth.uid() + or public.is_workflow_visible(id) + ); + +drop policy if exists "workflows_insert_owner" on public.workflows; +create policy "workflows_insert_owner" + on public.workflows for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "workflows_update_owner" on public.workflows; +create policy "workflows_update_owner" + on public.workflows for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "workflows_delete_owner" on public.workflows; +create policy "workflows_delete_owner" + on public.workflows for delete + to authenticated + using (user_id = auth.uid()); + +-- workflow_shares (4 policies) — D-05: SELECT visible to owner AND recipient +drop policy if exists "workflow_shares_select_owner_or_recipient" on public.workflow_shares; +create policy "workflow_shares_select_owner_or_recipient" + on public.workflow_shares for select + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_shares.workflow_id + and w.user_id = auth.uid() + ) + or lower(shared_with_email) = lower(auth.email()) + ); + +drop policy if exists "workflow_shares_insert_workflow_owner" on public.workflow_shares; +create policy "workflow_shares_insert_workflow_owner" + on public.workflow_shares for insert + to authenticated + with check ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + +drop policy if exists "workflow_shares_update_workflow_owner" on public.workflow_shares; +create policy "workflow_shares_update_workflow_owner" + on public.workflow_shares for update + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ) + with check ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + and shared_by_user_id = auth.uid() + ); + +drop policy if exists "workflow_shares_delete_workflow_owner" on public.workflow_shares; +create policy "workflow_shares_delete_workflow_owner" + on public.workflow_shares for delete + to authenticated + using ( + exists ( + select 1 + from public.workflows w + where w.id = workflow_id + and w.user_id = auth.uid() + ) + ); + +-- hidden_workflows (4 policies — owner-only) +drop policy if exists "hidden_workflows_select_owner" on public.hidden_workflows; +create policy "hidden_workflows_select_owner" + on public.hidden_workflows for select + to authenticated + using (user_id = auth.uid()); + +drop policy if exists "hidden_workflows_insert_owner" on public.hidden_workflows; +create policy "hidden_workflows_insert_owner" + on public.hidden_workflows for insert + to authenticated + with check (user_id = auth.uid()); + +drop policy if exists "hidden_workflows_update_owner" on public.hidden_workflows; +create policy "hidden_workflows_update_owner" + on public.hidden_workflows for update + to authenticated + using (user_id = auth.uid()) + with check (user_id = auth.uid()); + +drop policy if exists "hidden_workflows_delete_owner" on public.hidden_workflows; +create policy "hidden_workflows_delete_owner" + on public.hidden_workflows for delete + to authenticated + using (user_id = auth.uid()); diff --git a/backend/package-lock.json b/backend/package-lock.json index effa2adef..0025b4a12 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "mike-backend", "version": "1.0.0", + "license": "AGPL-3.0-only", "dependencies": { "@anthropic-ai/sdk": "^0.90.0", "@aws-sdk/client-s3": "^3.787.0", @@ -20,22 +21,34 @@ "express-rate-limit": "^8.5.1", "fast-diff": "^1.3.0", "fast-xml-parser": "^5.7.1", - "helmet": "^8.1.0", "jszip": "^3.10.1", "libreoffice-convert": "^1.6.0", + "lru-cache": "^11.3.5", "mammoth": "^1.9.0", - "multer": "^1.4.5-lts.2", + "multer": "^2.1.1", + "node-pg-migrate": "^8.0.4", + "p-limit": "^7.3.0", + "p-queue": "^9.2.0", "pdfjs-dist": "^4.10.38", - "resend": "^4.5.1" + "pg": "^8.20.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "resend": "^4.5.1", + "zod": "^4.4.2" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/multer": "^1.4.12", + "@types/multer": "^2.1.0", "@types/node": "^22.14.1", + "@types/pg": "^8.20.0", + "@types/supertest": "^7.2.0", + "pino-pretty": "^13.1.3", "prettier": "^3.8.1", + "supertest": "^7.2.2", "tsx": "^4.19.3", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^4.1.5" } }, "node_modules/@anthropic-ai/sdk": { @@ -972,6 +985,40 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -1437,6 +1484,22 @@ } } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/canvas": { "version": "0.1.97", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", @@ -1687,6 +1750,38 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "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", @@ -1699,6 +1794,32 @@ ], "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1781,6 +1902,270 @@ "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -2513,6 +2898,13 @@ "node": ">=18.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "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", @@ -2599,6 +2991,17 @@ "node": ">=20.0.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2610,6 +3013,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2620,7 +3034,14 @@ "@types/node": "*" } }, - "node_modules/@types/cors": { + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", @@ -2630,6 +3051,20 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -2663,6 +3098,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2671,9 +3113,9 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", - "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", "dev": true, "license": "MIT", "dependencies": { @@ -2689,6 +3131,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", @@ -2742,6 +3196,30 @@ "@types/node": "*" } }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz", + "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -2751,6 +3229,119 @@ "@types/node": "*" } }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", @@ -2782,6 +3373,30 @@ "node": ">= 14" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -2803,12 +3418,54 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2874,6 +3531,18 @@ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2935,21 +3604,107 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2971,6 +3726,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2986,6 +3748,13 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3009,6 +3778,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", @@ -3018,6 +3801,16 @@ "node": ">= 12" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3036,6 +3829,16 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3055,6 +3858,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dingbat-to-unicode": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", @@ -3198,6 +4022,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3207,6 +4037,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3237,6 +4077,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3249,6 +4096,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -3291,12 +4154,31 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3306,14 +4188,30 @@ "node": ">= 0.6" } }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", @@ -3376,6 +4274,13 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-copy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz", + "integrity": "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==", + "dev": true, + "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", @@ -3388,6 +4293,13 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "license": "Apache-2.0" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-xml-builder": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", @@ -3424,6 +4336,24 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -3465,6 +4395,39 @@ "node": ">= 0.8" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3477,6 +4440,24 @@ "node": ">=12.20.0" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3547,6 +4528,15 @@ "node": ">=18" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3597,6 +4587,30 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/google-auth-library": { "version": "10.6.2", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", @@ -3647,6 +4661,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -3669,14 +4699,12 @@ "node": ">= 0.4" } }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" }, "node_modules/html-to-text": { "version": "9.0.5", @@ -3820,12 +4848,52 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "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/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -3912,106 +4980,386 @@ "immediate": "~3.0.5" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/mammoth": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", - "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", - "license": "BSD-2-Clause", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" + "detect-libc": "^2.0.3" }, - "bin": { - "mammoth": "bin/mammoth" + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" + "node": ">= 12.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/mime-types": { - "version": "2.1.35", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", @@ -4028,25 +5376,38 @@ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "license": "ISC" }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { @@ -4056,22 +5417,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/nanoid": { @@ -4139,6 +5500,31 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-pg-migrate": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", + "integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==", + "license": "MIT", + "dependencies": { + "glob": "~11.1.0", + "yargs": "~17.7.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4160,6 +5546,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -4172,12 +5578,53 @@ "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", "license": "BSD-2-Clause" }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz", + "integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -4191,6 +5638,24 @@ "node": ">=8" } }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4243,12 +5708,44 @@ "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-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pdfjs-dist": { "version": "4.10.38", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", @@ -4270,6 +5767,276 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "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/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -4291,6 +6058,22 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/protobufjs": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", @@ -4328,6 +6111,17 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -4343,6 +6137,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4420,6 +6220,24 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "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", @@ -4451,6 +6269,40 @@ "node": ">= 4" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4471,6 +6323,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4493,6 +6354,23 @@ "license": "MIT", "peer": true }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", @@ -4562,6 +6440,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", @@ -4628,10 +6527,57 @@ "side-channel-map": "^1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" } }, "node_modules/sprintf-js": { @@ -4640,6 +6586,13 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4649,6 +6602,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -4672,6 +6632,45 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", @@ -4684,6 +6683,146 @@ ], "license": "MIT" }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -4812,6 +6951,174 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -4821,6 +7128,62 @@ "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", @@ -4877,6 +7240,63 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", + "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index 8451ab8b7..94626309d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,11 +1,22 @@ { "name": "mike-backend", "version": "1.0.0", + "license": "AGPL-3.0-only", "private": true, "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "prestart": "npm run db:migrate", + "start": "node dist/index.js", + "db:migrate": "node-pg-migrate -m migrations -j ts up", + "db:migrate-down": "node-pg-migrate -m migrations -j ts down 1", + "db:migrate-create": "node-pg-migrate -m migrations -j ts create", + "test:cross-tenant": "vitest run --config vitest.config.ts", + "test:no-db": "vitest run --config vitest.no-db.config.ts", + "test:golden-log": "vitest run --config vitest.golden-log.config.ts", + "test:docx": "vitest run --config vitest.docx.config.ts", + "test:auth-hardening": "vitest run --config vitest.auth-hardening.config.ts", + "test:saga": "vitest run --config vitest.saga.config.ts" }, "dependencies": { "@anthropic-ai/sdk": "^0.90.0", @@ -20,22 +31,33 @@ "express-rate-limit": "^8.5.1", "fast-diff": "^1.3.0", "fast-xml-parser": "^5.7.1", - "helmet": "^8.1.0", "jszip": "^3.10.1", "libreoffice-convert": "^1.6.0", + "lru-cache": "^11.3.5", "mammoth": "^1.9.0", - "multer": "^1.4.5-lts.2", + "multer": "^2.1.1", + "node-pg-migrate": "^8.0.4", + "p-limit": "^7.3.0", + "p-queue": "^9.2.0", "pdfjs-dist": "^4.10.38", - "resend": "^4.5.1" + "pg": "^8.20.0", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "resend": "^4.5.1", + "zod": "^4.4.2" }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", - "@types/multer": "^1.4.12", + "@types/multer": "^2.1.0", "@types/node": "^22.14.1", + "@types/pg": "^8.20.0", + "@types/supertest": "^7.2.0", + "pino-pretty": "^13.1.3", "prettier": "^3.8.1", + "supertest": "^7.2.2", "tsx": "^4.19.3", - "typescript": "^5.8.3" - }, - "license": "AGPL-3.0-only" + "typescript": "^5.8.3", + "vitest": "^4.1.5" + } } diff --git a/backend/schema.sql b/backend/schema.sql deleted file mode 100644 index cc9b9cef9..000000000 --- a/backend/schema.sql +++ /dev/null @@ -1,367 +0,0 @@ --- Mike Supabase schema --- Based on supabase-migration.sql plus the later backend/migrations/*.sql files. --- Use this for a fresh Supabase database. Existing deployments should continue --- to apply the incremental migration files instead. - -create extension if not exists "pgcrypto"; - --- --------------------------------------------------------------------------- --- User profiles --- --------------------------------------------------------------------------- - -create table if not exists public.user_profiles ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null unique references auth.users(id) on delete cascade, - display_name text, - organisation text, - tier text not null default 'Free', - message_credits_used integer not null default 0, - credits_reset_date timestamptz not null default (now() + interval '30 days'), - tabular_model text not null default 'gemini-3-flash-preview', - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_user_profiles_user - on public.user_profiles(user_id); - -create or replace function public.handle_new_user() -returns trigger -language plpgsql -security definer -set search_path = public -as $$ -begin - insert into public.user_profiles (user_id) - values (new.id) - on conflict (user_id) do nothing; - return new; -exception when others then - -- Never block signup if the profile insert fails. - return new; -end; -$$; - -drop trigger if exists on_auth_user_created on auth.users; -create trigger on_auth_user_created - after insert on auth.users - for each row execute procedure public.handle_new_user(); - -create table if not exists public.user_api_keys ( - id uuid primary key default gen_random_uuid(), - user_id uuid not null references auth.users(id) on delete cascade, - provider text not null check (provider in ('claude', 'gemini', 'openai')), - encrypted_key text not null, - iv text not null, - auth_tag text not null, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - unique(user_id, provider) -); - -create index if not exists idx_user_api_keys_user - on public.user_api_keys(user_id); - --- --------------------------------------------------------------------------- --- Projects and documents --- --------------------------------------------------------------------------- - -create table if not exists public.projects ( - id uuid primary key default gen_random_uuid(), - user_id text not null, - name text not null, - cm_number text, - visibility text not null default 'private', - shared_with jsonb not null default '[]'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_projects_user - on public.projects(user_id); - -create index if not exists projects_shared_with_idx - on public.projects using gin (shared_with); - -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, - user_id text not null, - name text not null, - parent_folder_id uuid references public.project_subfolders(id) on delete cascade, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_project_subfolders_project - on public.project_subfolders(project_id); - -create table if not exists public.documents ( - id uuid primary key default gen_random_uuid(), - project_id uuid references public.projects(id) on delete cascade, - user_id text not null, - filename text not null, - file_type text, - size_bytes integer not null default 0, - page_count integer, - structure_tree jsonb, - status text not null default 'pending', - folder_id uuid references public.project_subfolders(id) on delete set null, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_documents_user_project - on public.documents(user_id, project_id); - -create index if not exists idx_documents_project_folder - on public.documents(project_id, folder_id); - -create table if not exists public.document_versions ( - id uuid primary key default gen_random_uuid(), - document_id uuid not null references public.documents(id) on delete cascade, - storage_path text not null, - pdf_storage_path text, - source text not null default 'upload', - version_number integer, - display_name text, - created_at timestamptz not null default now(), - constraint document_versions_source_check - check (source = any (array[ - 'upload'::text, - 'user_upload'::text, - 'assistant_edit'::text, - 'user_accept'::text, - 'user_reject'::text, - 'generated'::text - ])) -); - -create index if not exists document_versions_document_id_idx - on public.document_versions(document_id, created_at desc); - -create index if not exists document_versions_doc_vnum_idx - on public.document_versions(document_id, version_number); - -alter table public.documents - add column if not exists current_version_id uuid - references public.document_versions(id) on delete set null; - -create table if not exists public.document_edits ( - id uuid primary key default gen_random_uuid(), - document_id uuid not null references public.documents(id) on delete cascade, - chat_message_id uuid, - version_id uuid not null references public.document_versions(id) on delete cascade, - change_id text not null, - del_w_id text, - ins_w_id text, - deleted_text text not null default '', - inserted_text text not null default '', - context_before text, - context_after text, - status text not null default 'pending' - check (status = any (array[ - 'pending'::text, - 'accepted'::text, - 'rejected'::text - ])), - created_at timestamptz not null default now(), - resolved_at timestamptz -); - -create index if not exists document_edits_document_id_idx - on public.document_edits(document_id, created_at desc); - -create index if not exists document_edits_message_id_idx - on public.document_edits(chat_message_id); - -create index if not exists document_edits_version_id_idx - on public.document_edits(version_id); - --- --------------------------------------------------------------------------- --- Workflows --- --------------------------------------------------------------------------- - -create table if not exists public.workflows ( - id uuid primary key default gen_random_uuid(), - user_id text, - title text not null, - type text not null, - prompt_md text, - columns_config jsonb, - practice text, - is_system boolean not null default false, - created_at timestamptz not null default now() -); - -create index if not exists idx_workflows_user - on public.workflows(user_id); - -create table if not exists public.hidden_workflows ( - id uuid primary key default gen_random_uuid(), - user_id text not null, - workflow_id text not null, - created_at timestamptz not null default now(), - unique(user_id, workflow_id) -); - -create index if not exists idx_hidden_workflows_user - on public.hidden_workflows(user_id); - -create table if not exists public.workflow_shares ( - id uuid primary key default gen_random_uuid(), - workflow_id uuid not null references public.workflows(id) on delete cascade, - shared_by_user_id text not null, - shared_with_email text not null, - allow_edit boolean not null default false, - created_at timestamptz not null default now(), - constraint workflow_shares_workflow_email_unique - unique(workflow_id, shared_with_email) -); - -create index if not exists workflow_shares_workflow_id_idx - on public.workflow_shares(workflow_id); - -create index if not exists workflow_shares_email_idx - on public.workflow_shares(shared_with_email); - --- --------------------------------------------------------------------------- --- Assistant chats --- --------------------------------------------------------------------------- - -create table if not exists public.chats ( - id uuid primary key default gen_random_uuid(), - project_id uuid references public.projects(id) on delete cascade, - user_id text not null, - title text, - created_at timestamptz not null default now() -); - -create index if not exists idx_chats_user - on public.chats(user_id); - -create index if not exists idx_chats_project - on public.chats(project_id); - -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, - role text not null, - content jsonb, - files jsonb, - annotations jsonb, - created_at timestamptz not null default now() -); - -create index if not exists idx_chat_messages_chat - on public.chat_messages(chat_id); - -do $$ -begin - if not exists ( - select 1 - from pg_constraint - where conname = 'document_edits_chat_message_id_fkey' - and conrelid = 'public.document_edits'::regclass - ) then - alter table public.document_edits - add constraint document_edits_chat_message_id_fkey - foreign key (chat_message_id) - references public.chat_messages(id) - on delete set null; - end if; -end; -$$; - --- --------------------------------------------------------------------------- --- Tabular reviews --- --------------------------------------------------------------------------- - -create table if not exists public.tabular_reviews ( - id uuid primary key default gen_random_uuid(), - project_id uuid references public.projects(id) on delete cascade, - user_id text not null, - title text, - columns_config jsonb, - workflow_id uuid references public.workflows(id) on delete set null, - practice text, - shared_with jsonb not null default '[]'::jsonb, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists idx_tabular_reviews_user - on public.tabular_reviews(user_id); - -create index if not exists idx_tabular_reviews_project - on public.tabular_reviews(project_id); - -create index if not exists tabular_reviews_shared_with_idx - on public.tabular_reviews using gin (shared_with); - -create table if not exists public.tabular_cells ( - id uuid primary key default gen_random_uuid(), - review_id uuid not null references public.tabular_reviews(id) on delete cascade, - document_id uuid not null references public.documents(id) on delete cascade, - column_index integer not null, - content text, - citations jsonb, - status text not null default 'pending', - created_at timestamptz not null default now() -); - -create index if not exists idx_tabular_cells_review - on public.tabular_cells(review_id, document_id, column_index); - -create table if not exists public.tabular_review_chats ( - id uuid primary key default gen_random_uuid(), - review_id uuid not null references public.tabular_reviews(id) on delete cascade, - user_id text not null, - title text, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() -); - -create index if not exists tabular_review_chats_review_idx - on public.tabular_review_chats(review_id, updated_at desc); - -create index if not exists tabular_review_chats_user_idx - on public.tabular_review_chats(user_id); - -create table if not exists public.tabular_review_chat_messages ( - id uuid primary key default gen_random_uuid(), - chat_id uuid not null references public.tabular_review_chats(id) on delete cascade, - role text not null, - content jsonb, - annotations jsonb, - created_at timestamptz not null default now() -); - -create index if not exists tabular_review_chat_messages_chat_idx - on public.tabular_review_chat_messages(chat_id, created_at); - --- --------------------------------------------------------------------------- --- Direct client grant hardening --- --------------------------------------------------------------------------- --- --- The frontend uses Supabase directly only for authentication. Application --- data access goes through the backend API with the service role after the --- backend verifies the user's JWT. Do not grant the browser anon/authenticated --- roles direct table privileges for backend-owned data. - -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; -revoke all on public.user_api_keys from anon, authenticated; diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 000000000..ad9264f0b --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 000000000..f6aed0d10 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,412 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "mike" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +# Uncomment to reject non-secure connections to the database. +# [db.ssl_enforcement] +# enabled = true + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[storage.buckets.hugo] +public = false +file_size_limit = "100MiB" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +# This feature is only available on the hosted platform. +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended. +# external_url = "" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to auth.external_url. +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +# Configure passkey sign-ins. +# [auth.passkey] +# enabled = false + +# Configure WebAuthn relying party settings (required when passkey is enabled). +# [auth.webauthn] +# rp_display_name = "Supabase" +# rp_id = "localhost" +# rp_origins = ["http://127.0.0.1:3000"] + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth callback URL derived from auth.external_url. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" + +# [experimental.pgdelta] +# When enabled, pg-delta becomes the active engine for supported schema flows. +# enabled = false +# Directory under `supabase/` where declarative files are written. +# declarative_schema_path = "./database" +# JSON string passed through to pg-delta SQL formatting. +# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}"