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
17 changes: 17 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
PORT=3001
FRONTEND_URL=http://localhost:3000
ROSS_ENV=local
# Comma-separated exact browser origins. FRONTEND_URL remains a compatibility fallback.
CORS_ALLOWED_ORIGINS=http://localhost:3000

# HMAC key used to sign /download/:token URLs. Required at startup.
# Generate with: openssl rand -hex 32
Expand All @@ -21,3 +24,17 @@ USER_API_KEYS_ENCRYPTION_SECRET=your-long-random-secret

# Optional: enables higher-rate CourtListener case law/citation lookup tools.
COURTLISTENER_API_TOKEN=your-courtlistener-token

# Licensed-source connectors are disabled by default. Do not enable without an
# executed provider agreement and an approved transport adapter.
CANLII_CONNECTOR_ENABLED=false
CANLII_CONTRACT_ID=
CANLII_ORGANIZATION_ID=
CANLII_API_KEY=
CANLII_API_BASE_URL=
CANLII_APPROVED_TRANSPORT=
CANLII_ALLOWED_OPERATIONS=
CANLII_FULL_TEXT_ENTITLED=false
CANLII_METADATA_RETENTION_DAYS=0
CANLII_FULL_TEXT_RETENTION_DAYS=0
CANLII_REDISTRIBUTION_ALLOWED=false
65 changes: 65 additions & 0 deletions backend/migrations/20260716_01_ontario_jurisdiction_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
-- ROSS-100: provider-neutral legal research settings with Ontario defaults.
--
-- The released legal_research_us column remains for backwards compatibility.
-- New application code reads the generic fields and derives the legacy flag.

alter table public.user_profiles
add column if not exists legal_research_enabled boolean not null default true,
add column if not exists default_country text not null default 'CA',
add column if not exists default_province text default 'ON',
add column if not exists enabled_jurisdictions text[] not null
default array['CA-ON', 'CA', 'US']::text[],
add column if not exists enabled_source_providers text[] not null
default array['a2aj-canada', 'ontario-elaws', 'justice-laws-canada', 'courtlistener-us']::text[];

do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'user_profiles_default_country_check'
and conrelid = 'public.user_profiles'::regclass
) then
alter table public.user_profiles
add constraint user_profiles_default_country_check
check (default_country in ('CA', 'US'));
end if;

if not exists (
select 1 from pg_constraint
where conname = 'user_profiles_default_province_check'
and conrelid = 'public.user_profiles'::regclass
) then
alter table public.user_profiles
add constraint user_profiles_default_province_check
check (default_province is null or default_province = 'ON');
end if;
end;
$$;

-- Respect an existing user's U.S. opt-out while adding Ontario and federal
-- Canada. No existing U.S. entitlement is removed.
update public.user_profiles
set
enabled_jurisdictions = case
when legal_research_us then array['CA-ON', 'CA', 'US']::text[]
else array['CA-ON', 'CA']::text[]
end,
enabled_source_providers = case
when legal_research_us then array['a2aj-canada', 'ontario-elaws', 'justice-laws-canada', 'courtlistener-us']::text[]
else array['a2aj-canada', 'ontario-elaws', 'justice-laws-canada']::text[]
end
where enabled_jurisdictions = array['CA-ON', 'CA', 'US']::text[]
and enabled_source_providers = array['a2aj-canada', 'ontario-elaws', 'justice-laws-canada', 'courtlistener-us']::text[];

alter table public.projects
add column if not exists jurisdictions text[] not null
default array['CA-ON', 'CA']::text[];

alter table public.chats
add column if not exists jurisdictions text[] not null
default array['CA-ON', 'CA']::text[],
add column if not exists legal_as_of_date date;

alter table public.workflows
alter column practice set default 'Civil Litigation',
alter column jurisdictions set default array['Canada / Ontario']::text[];
24 changes: 24 additions & 0 deletions backend/migrations/20260716_02_ontario_procedure_sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- ROSS-120: immutable audit records for official Ontario procedure-source
-- metadata checks. Source text and court forms are not copied into this table.

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)
);

alter table public.legal_source_version_checks enable row level security;

-- Checks are written and read only by the backend service role. No browser
-- grant or permissive RLS policy is intentionally created.
revoke all on table public.legal_source_version_checks from anon, authenticated;
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"test:legal-sources": "node --import tsx --test src/lib/legalSources/*.test.ts",
"start": "node dist/index.js"
},
"dependencies": {
Expand Down
12 changes: 10 additions & 2 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ create table if not exists public.user_profiles (
quote_model text,
mfa_on_login boolean not null default false,
legal_research_us boolean not null default true,
legal_research_enabled boolean not null default true,
default_country text not null default 'CA' check (default_country in ('CA', 'US')),
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[],
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
Expand Down Expand Up @@ -197,6 +202,7 @@ create table if not exists public.projects (
name text not null,
cm_number text,
practice text,
jurisdictions text[] not null default array['CA-ON', 'CA']::text[],
visibility text not null default 'private',
shared_with jsonb not null default '[]'::jsonb,
created_at timestamptz not null default now(),
Expand Down Expand Up @@ -335,8 +341,8 @@ create table if not exists public.workflows (
prompt_md text,
columns_config jsonb,
language text default 'English',
practice text default 'General Transactions',
jurisdictions text[] default array['General']::text[],
practice text default 'Civil Litigation',
jurisdictions text[] default array['Canada / Ontario']::text[],
created_at timestamptz not null default now()
);

Expand Down Expand Up @@ -474,6 +480,8 @@ create table if not exists public.chats (
project_id uuid references public.projects(id) on delete cascade,
user_id text not null,
title text,
jurisdictions text[] not null default array['CA-ON', 'CA']::text[],
legal_as_of_date date,
created_at timestamptz not null default now()
);

Expand Down
83 changes: 83 additions & 0 deletions backend/src/config/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
export type RossEnvironment = "local" | "test" | "staging" | "production";

export type RuntimeConfig = {
environment: RossEnvironment;
port: number;
allowedOrigins: string[];
};

const PLACEHOLDER = /(^|[.:/])(example\.invalid|localhost)([/:]|$)|your-|replace-with/i;

function cleanUrl(value: string, name: string): string {
let parsed: URL;
try {
parsed = new URL(value);
} catch {
throw new Error(`${name} must be an absolute URL.`);
}
if (!/^https?:$/.test(parsed.protocol)) {
throw new Error(`${name} must use http or https.`);
}
return parsed.origin;
}

export function parseAllowedOrigins(value?: string): string[] {
const configured = value?.trim() || "http://localhost:3000";
const origins = Array.from(
new Set(
configured
.split(",")
.map((origin) => origin.trim())
.filter(Boolean)
.map((origin) => cleanUrl(origin, "CORS_ALLOWED_ORIGINS")),
),
);
if (!origins.length) throw new Error("At least one CORS origin is required.");
return origins;
}

function requiredProductionValue(name: string): string {
const value = process.env[name]?.trim();
if (!value || PLACEHOLDER.test(value)) {
throw new Error(`${name} must be configured with a non-placeholder production value.`);
}
return value;
}

function environment(): RossEnvironment {
const value = (process.env.ROSS_ENV ?? process.env.NODE_ENV ?? "local").toLowerCase();
if (value === "development") return "local";
if (value === "local" || value === "test" || value === "staging" || value === "production") {
return value;
}
throw new Error(`Unsupported ROSS_ENV: ${value}`);
}

export function loadRuntimeConfig(): RuntimeConfig {
const currentEnvironment = environment();
const allowedOrigins = parseAllowedOrigins(
process.env.CORS_ALLOWED_ORIGINS ?? process.env.FRONTEND_URL,
);

if (currentEnvironment === "production") {
for (const name of [
"SUPABASE_URL",
"SUPABASE_SECRET_KEY",
"DOWNLOAD_SIGNING_SECRET",
"R2_ENDPOINT_URL",
"R2_ACCESS_KEY_ID",
"R2_SECRET_ACCESS_KEY",
"R2_BUCKET_NAME",
]) requiredProductionValue(name);
if (allowedOrigins.some((origin) => PLACEHOLDER.test(origin))) {
throw new Error("Production CORS origins cannot use localhost or placeholder domains.");
}
}

const requestedPort = Number.parseInt(process.env.PORT ?? "3001", 10);
return {
environment: currentEnvironment,
port: Number.isFinite(requestedPort) && requestedPort > 0 ? requestedPort : 3001,
allowedOrigins,
};
}
20 changes: 16 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import { workflowsRouter } from "./routes/workflows";
import { userRouter } from "./routes/user";
import { downloadsRouter } from "./routes/downloads";
import { caseLawRouter } from "./routes/caseLaw";
import { legalSourcesRouter } from "./routes/legalSources";
import { loadRuntimeConfig } from "./config/runtime";

const app = express();
const PORT = process.env.PORT ?? 3001;
const runtime = loadRuntimeConfig();
const PORT = runtime.port;
const isProduction = process.env.NODE_ENV === "production";

function envInt(name: string, fallback: number): number {
Expand Down Expand Up @@ -113,7 +116,13 @@ app.use(

app.use(
cors({
origin: process.env.FRONTEND_URL ?? "http://localhost:3000",
origin(origin, callback) {
if (!origin || runtime.allowedOrigins.includes(origin)) {
callback(null, true);
return;
}
callback(new Error("Origin is not allowed by ROSS CORS policy."));
},
credentials: true,
}),
);
Expand Down Expand Up @@ -155,9 +164,12 @@ app.use("/user", userRouter);
app.use("/users", userRouter);
app.use("/download", downloadsRouter);
app.use("/case-law", caseLawRouter);
app.use("/legal-sources", legalSourcesRouter);

app.get("/health", (_req, res) => res.json({ ok: true }));
app.get("/health", (_req, res) =>
res.json({ ok: true, service: "ross-api", environment: runtime.environment }),
);

app.listen(PORT, () => {
console.log(`Mike backend running on port ${PORT}`);
console.log(`ROSS API running on port ${PORT} (${runtime.environment})`);
});
6 changes: 3 additions & 3 deletions backend/src/lib/chat/contextBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type AskInputResponseItem,
devLog,
} from "./types";
import { buildSystemPrompt } from "./prompts";
import { buildSystemPrompt, type ResearchPromptSettings } from "./prompts";
import { parseCitations, createCitation } from "./citations";
import type { AssistantEvent } from "./streaming";

Expand Down Expand Up @@ -131,10 +131,10 @@ export function buildMessages(
}[],
systemPromptExtra?: string,
docIndex?: DocIndex,
includeResearchTools = true,
researchSettings: boolean | ResearchPromptSettings = true,
) {
const formatted: unknown[] = [];
let systemContent = buildSystemPrompt(includeResearchTools);
let systemContent = buildSystemPrompt(researchSettings);

if (systemPromptExtra) {
systemContent += `\n\n${systemPromptExtra.trim()}`;
Expand Down
Loading
Loading