Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.github
**/.env
**/.env.*
!**/.env.example
!**/.env.local.example
**/node_modules
**/.next
**/.open-next
**/dist
**/out
**/coverage
*.log

176 changes: 176 additions & 0 deletions .github/workflows/deploy-private-ross.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
name: Deploy private ROSS

on:
workflow_dispatch:
inputs:
fly_organization:
description: Fly.io organization slug shown in the Fly dashboard
required: true
default: personal
type: string
api_app_name:
description: Globally unique Fly.io name for the ROSS API
required: true
default: ross-ranadeoss-api
type: string
web_app_name:
description: Globally unique Fly.io name for the private ROSS website
required: true
default: ross-ranadeoss-private
type: string

permissions:
contents: read

concurrency:
group: deploy-private-ross
cancel-in-progress: false

jobs:
deploy:
name: Deploy owner-only ROSS to Toronto
runs-on: ubuntu-latest
environment: private-online
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
ROSS_SUPABASE_URL: ${{ secrets.ROSS_SUPABASE_URL }}
ROSS_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.ROSS_SUPABASE_PUBLISHABLE_KEY }}
ROSS_SUPABASE_SECRET_KEY: ${{ secrets.ROSS_SUPABASE_SECRET_KEY }}
ROSS_S3_ENDPOINT_URL: ${{ secrets.ROSS_S3_ENDPOINT_URL }}
ROSS_S3_REGION: ${{ secrets.ROSS_S3_REGION }}
ROSS_S3_ACCESS_KEY_ID: ${{ secrets.ROSS_S3_ACCESS_KEY_ID }}
ROSS_S3_SECRET_ACCESS_KEY: ${{ secrets.ROSS_S3_SECRET_ACCESS_KEY }}
API_APP: ${{ inputs.api_app_name }}
WEB_APP: ${{ inputs.web_app_name }}
FLY_ORG: ${{ inputs.fly_organization }}
PUBLIC_WEBSITE_URL: https://ross-ontario.augustmaat.chatgpt.site
steps:
- uses: actions/checkout@v4

- name: Validate deployment inputs and secrets
shell: bash
run: |
set -euo pipefail
for name in \
FLY_API_TOKEN \
ROSS_SUPABASE_URL \
ROSS_SUPABASE_PUBLISHABLE_KEY \
ROSS_SUPABASE_SECRET_KEY \
ROSS_S3_ENDPOINT_URL \
ROSS_S3_REGION \
ROSS_S3_ACCESS_KEY_ID \
ROSS_S3_SECRET_ACCESS_KEY; do
if [ -z "${!name:-}" ]; then
echo "Required GitHub Actions secret is missing: ${name}" >&2
exit 1
fi
done
for value in "$API_APP" "$WEB_APP"; do
if ! [[ "$value" =~ ^[a-z0-9][a-z0-9-]{2,61}[a-z0-9]$ ]]; then
echo "Fly app names must use 4-63 lowercase letters, numbers, or hyphens." >&2
exit 1
fi
done
if [ "$API_APP" = "$WEB_APP" ]; then
echo "The API and website app names must be different." >&2
exit 1
fi

- uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: 0.4.49

- name: Create Fly applications when absent
shell: bash
run: |
set -euo pipefail
if ! flyctl status --app "$API_APP" >/dev/null 2>&1; then
flyctl apps create "$API_APP" --org "$FLY_ORG"
fi
if ! flyctl status --app "$WEB_APP" >/dev/null 2>&1; then
flyctl apps create "$WEB_APP" --org "$FLY_ORG"
fi

- name: Configure private API secrets
shell: bash
run: |
set -euo pipefail
WEB_URL="https://${WEB_APP}.fly.dev"
API_URL="https://${API_APP}.fly.dev"

flyctl secrets set --stage --app "$API_APP" \
"SUPABASE_URL=${ROSS_SUPABASE_URL}" \
"SUPABASE_SECRET_KEY=${ROSS_SUPABASE_SECRET_KEY}" \
"R2_ENDPOINT_URL=${ROSS_S3_ENDPOINT_URL}" \
"R2_REGION=${ROSS_S3_REGION}" \
"R2_ACCESS_KEY_ID=${ROSS_S3_ACCESS_KEY_ID}" \
"R2_SECRET_ACCESS_KEY=${ROSS_S3_SECRET_ACCESS_KEY}" \
"R2_BUCKET_NAME=ross-private-files" \
"ROSS_ENV=staging" \
"ROSS_HOSTED_MODE=controlled-beta" \
"HOSTED_MODEL_PROVIDERS=openai" \
"ROSS_DATA_BOUNDARY_VERSION=2026-07-16" \
"CORS_ALLOWED_ORIGINS=${WEB_URL}" \
"FRONTEND_URL=${WEB_URL}" \
"API_PUBLIC_URL=${API_URL}"

ensure_random_secret() {
local key="$1"
if ! flyctl secrets list --app "$API_APP" --json \
| jq -e --arg key "$key" '.[] | select((.Name // .name) == $key)' \
>/dev/null; then
flyctl secrets set --stage --app "$API_APP" \
"${key}=$(openssl rand -hex 32)"
fi
}

ensure_random_secret DOWNLOAD_SIGNING_SECRET
ensure_random_secret USER_API_KEYS_ENCRYPTION_SECRET
ensure_random_secret MCP_CONNECTORS_ENCRYPTION_SECRET

- name: Deploy ROSS API
run: >-
flyctl deploy .
--config deploy/fly/api.toml
--app "$API_APP"
--remote-only
--ha=false

- name: Deploy private ROSS website
shell: bash
run: |
set -euo pipefail
WEB_URL="https://${WEB_APP}.fly.dev"
API_URL="https://${API_APP}.fly.dev"
flyctl deploy . \
--config deploy/fly/frontend.toml \
--app "$WEB_APP" \
--remote-only \
--ha=false \
--build-arg "NEXT_PUBLIC_SUPABASE_URL=${ROSS_SUPABASE_URL}" \
--build-arg "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=${ROSS_SUPABASE_PUBLISHABLE_KEY}" \
--build-arg "NEXT_PUBLIC_API_BASE_URL=${API_URL}" \
--build-arg "NEXT_PUBLIC_ROSS_APP_URL=${WEB_URL}" \
--build-arg "NEXT_PUBLIC_ROSS_WEBSITE_URL=${PUBLIC_WEBSITE_URL}" \
--build-arg "NEXT_PUBLIC_ROSS_HOSTED_MODE=controlled-beta" \
--build-arg "NEXT_PUBLIC_ROSS_DATA_BOUNDARY_VERSION=2026-07-16" \
--build-arg "NEXT_PUBLIC_ROSS_SIGNUPS_ENABLED=false"

- name: Verify owner-only services
shell: bash
run: |
set -euo pipefail
API_URL="https://${API_APP}.fly.dev"
WEB_URL="https://${WEB_APP}.fly.dev"
curl --fail --retry 8 --retry-delay 5 --retry-all-errors \
"${API_URL}/health"
curl --fail --retry 8 --retry-delay 5 --retry-all-errors \
"${WEB_URL}/login" >/dev/null
{
echo "## Private ROSS deployed"
echo
echo "- Login: ${WEB_URL}/login"
echo "- API health: ${API_URL}/health"
echo "- Region: Toronto (yyz)"
echo "- New sign-ups: disabled"
} >> "$GITHUB_STEP_SUMMARY"
3 changes: 2 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SECRET_KEY=your-supabase-service-role-key

R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com
R2_REGION=auto
R2_ACCESS_KEY_ID=your-r2-access-key
R2_SECRET_ACCESS_KEY=your-r2-secret-key
R2_BUCKET_NAME=mike
R2_BUCKET_NAME=ross-private-files

GEMINI_API_KEY=your-gemini-key
ANTHROPIC_API_KEY=your-anthropic-key
Expand Down
72 changes: 72 additions & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ create table if not exists public.user_profiles (
default_province text default 'ON' check (default_province is null or default_province = 'ON'),
enabled_jurisdictions text[] not null default array['CA-ON', 'CA', 'US']::text[],
enabled_source_providers text[] not null default array['a2aj-canada', 'ontario-elaws', 'justice-laws-canada', 'courtlistener-us']::text[],
beta_data_boundary_version text,
beta_data_boundary_acknowledged_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
Expand Down Expand Up @@ -794,6 +796,45 @@ create table if not exists public.tabular_review_chat_messages (
create index if not exists tabular_review_chat_messages_chat_idx
on public.tabular_review_chat_messages(chat_id, created_at);

-- ---------------------------------------------------------------------------
-- Ontario legal-source operations and metadata-only security audit
-- ---------------------------------------------------------------------------

create table if not exists public.legal_source_version_checks (
id bigint generated by default as identity primary key,
source_id text not null,
source_url text not null,
checked_at timestamptz not null,
reachable boolean not null,
etag text,
last_modified text,
metadata_hash text not null,
created_at timestamptz not null default now(),
constraint legal_source_version_checks_metadata_hash_format
check (metadata_hash ~ '^[a-f0-9]{64}$'),
constraint legal_source_version_checks_source_metadata_unique
unique (source_id, metadata_hash)
);

create table if not exists public.security_audit_events (
id uuid primary key default gen_random_uuid(),
occurred_at timestamptz not null default now(),
actor_user_id uuid references auth.users(id) on delete set null,
event_type text not null,
resource_type text,
resource_id text,
metadata jsonb not null default '{}'::jsonb,
constraint security_audit_event_type_format
check (event_type ~ '^[a-z][a-z0-9_.-]{2,79}$'),
constraint security_audit_metadata_object
check (jsonb_typeof(metadata) = 'object')
);

create index if not exists security_audit_events_actor_time_idx
on public.security_audit_events (actor_user_id, occurred_at desc);
create index if not exists security_audit_events_type_time_idx
on public.security_audit_events (event_type, occurred_at desc);

-- ---------------------------------------------------------------------------
-- CourtListener bulk-data indexes
-- ---------------------------------------------------------------------------
Expand Down Expand Up @@ -842,6 +883,35 @@ alter table public.courtlistener_opinion_cluster_index enable row level security
-- backend verifies the user's JWT. Do not grant the browser anon/authenticated
-- roles direct table privileges for backend-owned data.

-- Defence in depth: every table in the exposed public schema has RLS enabled.
-- No permissive browser policy is created because application data must pass
-- through the authenticated backend. The Supabase service role bypasses RLS.
alter table public.user_profiles enable row level security;
alter table public.user_api_keys enable row level security;
alter table public.user_mcp_connectors enable row level security;
alter table public.user_mcp_oauth_tokens enable row level security;
alter table public.user_mcp_oauth_states enable row level security;
alter table public.user_mcp_connector_tools enable row level security;
alter table public.user_mcp_tool_audit_logs enable row level security;
alter table public.projects enable row level security;
alter table public.project_subfolders enable row level security;
alter table public.documents enable row level security;
alter table public.document_versions enable row level security;
alter table public.document_edits enable row level security;
alter table public.workflows enable row level security;
alter table public.hidden_workflows enable row level security;
alter table public.workflow_shares enable row level security;
alter table public.chats enable row level security;
alter table public.chat_messages enable row level security;
alter table public.tabular_reviews enable row level security;
alter table public.tabular_cells enable row level security;
alter table public.tabular_review_chats enable row level security;
alter table public.tabular_review_chat_messages enable row level security;
alter table public.legal_source_version_checks enable row level security;
alter table public.security_audit_events enable row level security;
alter table public.courtlistener_citation_index enable row level security;
alter table public.courtlistener_opinion_cluster_index enable row level security;

revoke all on public.user_profiles from anon, authenticated;
revoke all on public.projects from anon, authenticated;
revoke all on public.project_subfolders from anon, authenticated;
Expand All @@ -863,5 +933,7 @@ revoke all on public.user_mcp_oauth_tokens from anon, authenticated;
revoke all on public.user_mcp_oauth_states from anon, authenticated;
revoke all on public.user_mcp_connector_tools from anon, authenticated;
revoke all on public.user_mcp_tool_audit_logs from anon, authenticated;
revoke all on public.legal_source_version_checks from anon, authenticated;
revoke all on public.security_audit_events from anon, authenticated;
revoke all on public.courtlistener_citation_index from anon, authenticated;
revoke all on public.courtlistener_opinion_cluster_index from anon, authenticated;
3 changes: 3 additions & 0 deletions backend/src/config/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const KEYS = [
"SUPABASE_SECRET_KEY",
"DOWNLOAD_SIGNING_SECRET",
"R2_ENDPOINT_URL",
"R2_REGION",
"R2_ACCESS_KEY_ID",
"R2_SECRET_ACCESS_KEY",
"R2_BUCKET_NAME",
Expand Down Expand Up @@ -100,6 +101,7 @@ test("non-local raw model logging and unapproved production fail closed", () =>
SUPABASE_SECRET_KEY: "production-secret-value",
DOWNLOAD_SIGNING_SECRET: "production-signing-value",
R2_ENDPOINT_URL: "https://objects.ross.test",
R2_REGION: "ca-central-1",
R2_ACCESS_KEY_ID: "production-access-value",
R2_SECRET_ACCESS_KEY: "production-storage-secret",
R2_BUCKET_NAME: "ross-production",
Expand All @@ -126,6 +128,7 @@ test("production requires a valid immutable release manifest identity", () => {
SUPABASE_SECRET_KEY: "production-secret-value",
DOWNLOAD_SIGNING_SECRET: "production-signing-value",
R2_ENDPOINT_URL: "https://objects.ross.test",
R2_REGION: "ca-central-1",
R2_ACCESS_KEY_ID: "production-access-value",
R2_SECRET_ACCESS_KEY: "production-storage-secret",
R2_BUCKET_NAME: "ross-production",
Expand Down
1 change: 1 addition & 0 deletions backend/src/config/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function loadRuntimeConfig(): RuntimeConfig {
"SUPABASE_SECRET_KEY",
"DOWNLOAD_SIGNING_SECRET",
"R2_ENDPOINT_URL",
"R2_REGION",
"R2_ACCESS_KEY_ID",
"R2_SECRET_ACCESS_KEY",
"R2_BUCKET_NAME",
Expand Down
7 changes: 4 additions & 3 deletions backend/src/lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* Cloudflare R2 storage utilities for Mike document management.
* R2 is S3-compatible — uses @aws-sdk/client-s3.
* S3-compatible storage utilities for ROSS document management.
* Supports Cloudflare R2, Supabase Storage S3, and compatible providers.
*
* Required env vars:
* R2_ENDPOINT_URL — https://<account-id>.r2.cloudflarestorage.com
* R2_REGION — signing region ("auto" for R2; project region for Supabase)
* R2_ACCESS_KEY_ID — R2 API token (Access Key ID)
* R2_SECRET_ACCESS_KEY — R2 API token (Secret Access Key)
* R2_BUCKET_NAME — bucket name (default: "mike")
Expand All @@ -25,7 +26,7 @@ let cachedClient: S3Client | undefined;
function getClient(): S3Client {
if (!cachedClient) {
cachedClient = new S3Client({
region: "auto",
region: process.env.R2_REGION?.trim() || "auto",
endpoint: process.env.R2_ENDPOINT_URL!,
forcePathStyle: true,
credentials: {
Expand Down
35 changes: 35 additions & 0 deletions deploy/fly/api.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
primary_region = "yyz"
kill_signal = "SIGTERM"
kill_timeout = "30s"

[build]
dockerfile = "deploy/fly/backend.Dockerfile"

[env]
NODE_ENV = "production"
PORT = "3001"

[http_service]
internal_port = 3001
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
processes = ["app"]

[http_service.concurrency]
type = "requests"
soft_limit = 20
hard_limit = 30

[[http_service.checks]]
grace_period = "30s"
interval = "30s"
method = "GET"
path = "/health"
timeout = "10s"

[[vm]]
size = "shared-cpu-1x"
memory = "1gb"

Loading
Loading