← back to Norma Platform
security(cycle-2): pin retired gemini-2.0-flash -> 2.5-flash (35 files); /api/users admin-only (was staff-readable); rate-limit public petition-sign; retire legacy-token->admin path; guard gmail/send null-org; ingest-news reports failure honestly (no more success:true on 0-inserted)
e390f1af370e958e5f8995073b7b48da2e46123c · 2026-08-05 13:51:33 -0700 · Steve
Files touched
M app/api/agents/advocacy/generate/route.tsM app/api/agents/petition/generate/route.tsM app/api/ai-chat/route.tsM app/api/ai/generate/route.tsM app/api/collaborations/discover/route.tsM app/api/contacts/enrich/route.tsM app/api/cron/ingest-news/route.tsM app/api/drafts/auto-generate/route.tsM app/api/email-analyzer/fetch-url/route.tsM app/api/email-analyzer/rewrite/route.tsM app/api/email-analyzer/score/route.tsM app/api/email-analyzer/subject-lines/generate/route.tsM app/api/gmail/draft-reply/route.tsM app/api/gmail/send/route.tsM app/api/grants/[id]/proposals/generate/route.tsM app/api/journalists/scrape/route.tsM app/api/leads/discover/route.tsM app/api/library/generate/route.tsM app/api/member-email/route.tsM app/api/orgs/enrich/route.tsM app/api/outreach-pipeline/generate/route.tsM app/api/outreach/generate/route.tsM app/api/petitions/draft/route.tsM app/api/petitions/ideas/route.tsM app/api/pipeline/generate/route.tsM app/api/pulse/petitions/[id]/sign/route.tsM app/api/scraper/suggestions/route.tsM app/api/social/ai-compose/route.tsM app/api/statements/generate/route.tsM app/api/users/route.tsM app/api/webhooks/pulse-topic/route.tsM app/api/xsessions/route.tsM lib/api-registry.tsM lib/auth.tsM lib/chat.tsM lib/detect-mentions.tsM lib/generator.tsM lib/ingest-news.tsM lib/petition-engine.ts
Diff
commit e390f1af370e958e5f8995073b7b48da2e46123c
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 5 13:51:33 2026 -0700
security(cycle-2): pin retired gemini-2.0-flash -> 2.5-flash (35 files); /api/users admin-only (was staff-readable); rate-limit public petition-sign; retire legacy-token->admin path; guard gmail/send null-org; ingest-news reports failure honestly (no more success:true on 0-inserted)
---
app/api/agents/advocacy/generate/route.ts | 2 +-
app/api/agents/petition/generate/route.ts | 2 +-
app/api/ai-chat/route.ts | 4 +--
app/api/ai/generate/route.ts | 2 +-
app/api/collaborations/discover/route.ts | 2 +-
app/api/contacts/enrich/route.ts | 2 +-
app/api/cron/ingest-news/route.ts | 16 ++++++++---
app/api/drafts/auto-generate/route.ts | 2 +-
app/api/email-analyzer/fetch-url/route.ts | 2 +-
app/api/email-analyzer/rewrite/route.ts | 2 +-
app/api/email-analyzer/score/route.ts | 2 +-
.../email-analyzer/subject-lines/generate/route.ts | 2 +-
app/api/gmail/draft-reply/route.ts | 2 +-
app/api/gmail/send/route.ts | 10 ++++++-
app/api/grants/[id]/proposals/generate/route.ts | 2 +-
app/api/journalists/scrape/route.ts | 2 +-
app/api/leads/discover/route.ts | 2 +-
app/api/library/generate/route.ts | 2 +-
app/api/member-email/route.ts | 2 +-
app/api/orgs/enrich/route.ts | 6 ++---
app/api/outreach-pipeline/generate/route.ts | 2 +-
app/api/outreach/generate/route.ts | 2 +-
app/api/petitions/draft/route.ts | 4 +--
app/api/petitions/ideas/route.ts | 2 +-
app/api/pipeline/generate/route.ts | 6 ++---
app/api/pulse/petitions/[id]/sign/route.ts | 11 ++++++++
app/api/scraper/suggestions/route.ts | 2 +-
app/api/social/ai-compose/route.ts | 2 +-
app/api/statements/generate/route.ts | 4 +--
app/api/users/route.ts | 4 ++-
app/api/webhooks/pulse-topic/route.ts | 6 ++---
app/api/xsessions/route.ts | 2 +-
lib/api-registry.ts | 2 +-
lib/auth.ts | 31 +++++-----------------
lib/chat.ts | 2 +-
lib/detect-mentions.ts | 2 +-
lib/generator.ts | 2 +-
lib/ingest-news.ts | 2 +-
lib/petition-engine.ts | 4 +--
39 files changed, 85 insertions(+), 75 deletions(-)
diff --git a/app/api/agents/advocacy/generate/route.ts b/app/api/agents/advocacy/generate/route.ts
index d8beead..a8cd122 100644
--- a/app/api/agents/advocacy/generate/route.ts
+++ b/app/api/agents/advocacy/generate/route.ts
@@ -7,7 +7,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[agents/advocacy/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
export async function POST(request: NextRequest) {
diff --git a/app/api/agents/petition/generate/route.ts b/app/api/agents/petition/generate/route.ts
index 9ccf220..483b2c8 100644
--- a/app/api/agents/petition/generate/route.ts
+++ b/app/api/agents/petition/generate/route.ts
@@ -7,7 +7,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[agents/petition/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
export async function POST(request: NextRequest) {
diff --git a/app/api/ai-chat/route.ts b/app/api/ai-chat/route.ts
index 8843053..7e23407 100644
--- a/app/api/ai-chat/route.ts
+++ b/app/api/ai-chat/route.ts
@@ -316,7 +316,7 @@ When an action was taken, confirm it clearly. Be concise, friendly, and data-dri
If asked about something not in your data, say so honestly.`;
/* ── Call Gemini ─────────────────────────────────────────────────────── */
- const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
+ const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`;
const geminiRes = await fetch(geminiUrl, {
method: 'POST',
@@ -360,7 +360,7 @@ If asked about something not in your data, say so honestly.`;
[
reply,
JSON.stringify({
- model: 'gemini-2.0-flash',
+ model: 'gemini-2.5-flash',
action: action || null,
context_tables: ['drafts', 'partners', 'news_items'],
}),
diff --git a/app/api/ai/generate/route.ts b/app/api/ai/generate/route.ts
index ca93c02..7801342 100644
--- a/app/api/ai/generate/route.ts
+++ b/app/api/ai/generate/route.ts
@@ -15,7 +15,7 @@ import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent`;
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
diff --git a/app/api/collaborations/discover/route.ts b/app/api/collaborations/discover/route.ts
index e71994b..22449f2 100644
--- a/app/api/collaborations/discover/route.ts
+++ b/app/api/collaborations/discover/route.ts
@@ -4,7 +4,7 @@ import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { getBrand } from '@/lib/brand';
-const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
+const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
/**
* POST /api/collaborations/discover
diff --git a/app/api/contacts/enrich/route.ts b/app/api/contacts/enrich/route.ts
index e902d82..9f4d691 100644
--- a/app/api/contacts/enrich/route.ts
+++ b/app/api/contacts/enrich/route.ts
@@ -40,7 +40,7 @@ If you have no information about this person, return:
try {
const res = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`,
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
diff --git a/app/api/cron/ingest-news/route.ts b/app/api/cron/ingest-news/route.ts
index 57fbbd0..a2e4594 100644
--- a/app/api/cron/ingest-news/route.ts
+++ b/app/api/cron/ingest-news/route.ts
@@ -17,10 +17,18 @@ export async function POST(request: NextRequest) {
`[cron/ingest-news] Complete — inserted=${result.inserted} skipped=${result.skipped} total=${result.total}`
);
- return NextResponse.json({
- success: true,
- ...result,
- });
+ // Don't report success when EVERY item failed to insert (e.g. an FK/DB
+ // error swallowed per-item). Silent "success:true, inserted:0" masks a
+ // fully-broken ingest that starves every downstream feature.
+ const totalFailure =
+ result.total > 0 && result.inserted === 0 && (result.skipped ?? 0) === 0;
+ if (totalFailure) {
+ console.error(`[cron/ingest-news] TOTAL FAILURE — 0 inserted of ${result.total} items`);
+ }
+ return NextResponse.json(
+ { success: !totalFailure, ...result },
+ { status: totalFailure ? 500 : 200 },
+ );
} catch (err) {
console.error('[cron/ingest-news] Failed:', (err as Error).message);
return NextResponse.json(
diff --git a/app/api/drafts/auto-generate/route.ts b/app/api/drafts/auto-generate/route.ts
index df575f1..6bab631 100644
--- a/app/api/drafts/auto-generate/route.ts
+++ b/app/api/drafts/auto-generate/route.ts
@@ -14,7 +14,7 @@ function getGeminiUrl(): string {
if (!GEMINI_KEY) {
throw new Error('[drafts/auto-generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
}
async function callGemini(prompt: string): Promise<string> {
diff --git a/app/api/email-analyzer/fetch-url/route.ts b/app/api/email-analyzer/fetch-url/route.ts
index 495b98e..f30ceb6 100644
--- a/app/api/email-analyzer/fetch-url/route.ts
+++ b/app/api/email-analyzer/fetch-url/route.ts
@@ -10,7 +10,7 @@ import { assertPublicUrl } from '@/lib/ssrf-guard';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const GEMINI_URL =
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
+ 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
/* ─── HTML parsing helpers (regex-based, no external deps) ──────────────── */
diff --git a/app/api/email-analyzer/rewrite/route.ts b/app/api/email-analyzer/rewrite/route.ts
index 15eb168..e6a4b36 100644
--- a/app/api/email-analyzer/rewrite/route.ts
+++ b/app/api/email-analyzer/rewrite/route.ts
@@ -37,7 +37,7 @@ async function buildUniversalRefBlock(): Promise<string> {
}
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
+const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
diff --git a/app/api/email-analyzer/score/route.ts b/app/api/email-analyzer/score/route.ts
index 30b3e8f..e80e70c 100644
--- a/app/api/email-analyzer/score/route.ts
+++ b/app/api/email-analyzer/score/route.ts
@@ -14,7 +14,7 @@ import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
+const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
export interface ScoreItem {
key: string;
diff --git a/app/api/email-analyzer/subject-lines/generate/route.ts b/app/api/email-analyzer/subject-lines/generate/route.ts
index db0ab38..2efa2cd 100644
--- a/app/api/email-analyzer/subject-lines/generate/route.ts
+++ b/app/api/email-analyzer/subject-lines/generate/route.ts
@@ -12,7 +12,7 @@ import { checkRateLimit } from '@/lib/rate-limit';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const GEMINI_URL =
- 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
+ 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
diff --git a/app/api/gmail/draft-reply/route.ts b/app/api/gmail/draft-reply/route.ts
index 7bc1964..e766a0c 100644
--- a/app/api/gmail/draft-reply/route.ts
+++ b/app/api/gmail/draft-reply/route.ts
@@ -63,7 +63,7 @@ NEXT_ACTIONS: [recommended actions, comma-separated]
DRAFT:
[the reply text — professional email body only, no greeting line with the name unless you can determine it from the email]`;
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`;
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`;
const geminiRes = await fetch(url, {
method: 'POST',
diff --git a/app/api/gmail/send/route.ts b/app/api/gmail/send/route.ts
index 37b70f8..68d7dab 100644
--- a/app/api/gmail/send/route.ts
+++ b/app/api/gmail/send/route.ts
@@ -24,6 +24,14 @@ export async function POST(request: NextRequest) {
if (auth instanceof NextResponse) return auth;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
+ // Require an org context — a null org produced orphaned campaigns and let a
+ // caller send with no tenant scoping. Admins pass x-org-id; staff inherit theirs.
+ if (!orgId) {
+ return NextResponse.json(
+ { error: 'No organization context — set x-org-id (admin) or use an org-scoped account.' },
+ { status: 400 },
+ );
+ }
const body = await request.json();
const { messageId, body_html, body_text, action, to, subject } = body;
@@ -84,7 +92,7 @@ export async function POST(request: NextRequest) {
sent_at, org_id
) VALUES (
$1, $2, $3, $4, $5,
- $6, 'gmail_reply', 'professional', true, 'gemini-2.0-flash',
+ $6, 'gmail_reply', 'professional', true, 'gemini-2.5-flash',
${status === 'sent' ? 'NOW()' : 'NULL'}, $7
) RETURNING id, status, created_at`,
[
diff --git a/app/api/grants/[id]/proposals/generate/route.ts b/app/api/grants/[id]/proposals/generate/route.ts
index 4744e93..ae849a2 100644
--- a/app/api/grants/[id]/proposals/generate/route.ts
+++ b/app/api/grants/[id]/proposals/generate/route.ts
@@ -6,7 +6,7 @@ import { getBrand } from '@/lib/brand';
import { getOrgId } from '@/lib/orgId';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_MODEL = 'gemini-2.0-flash';
+const GEMINI_MODEL = 'gemini-2.5-flash';
/**
* POST /api/grants/[id]/proposals/generate
diff --git a/app/api/journalists/scrape/route.ts b/app/api/journalists/scrape/route.ts
index fdf943f..c4c4baa 100644
--- a/app/api/journalists/scrape/route.ts
+++ b/app/api/journalists/scrape/route.ts
@@ -4,7 +4,7 @@ import { requireRole } from '@/lib/require-role';
import { createJournalistLinks } from '@/lib/journalist-links';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
// ─── RSS helpers (adapted from lib/ingest-news.ts) ─────────────────────────
diff --git a/app/api/leads/discover/route.ts b/app/api/leads/discover/route.ts
index eeccbdf..da9d974 100644
--- a/app/api/leads/discover/route.ts
+++ b/app/api/leads/discover/route.ts
@@ -3,7 +3,7 @@ import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getBrand } from '@/lib/brand';
-const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
+const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
/**
* POST /api/leads/discover
diff --git a/app/api/library/generate/route.ts b/app/api/library/generate/route.ts
index a10794f..eb86c8a 100644
--- a/app/api/library/generate/route.ts
+++ b/app/api/library/generate/route.ts
@@ -4,7 +4,7 @@ import { getBrand } from '@/lib/brand';
import { getOrgId } from '@/lib/orgId';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
/**
* POST /api/library/generate
diff --git a/app/api/member-email/route.ts b/app/api/member-email/route.ts
index 2c71d70..90c8d8d 100644
--- a/app/api/member-email/route.ts
+++ b/app/api/member-email/route.ts
@@ -22,7 +22,7 @@ import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent`;
const EMAIL_TYPE_LABELS: Record<string, string> = {
'breaking-news': 'Breaking News Alert',
diff --git a/app/api/orgs/enrich/route.ts b/app/api/orgs/enrich/route.ts
index 0964d10..598855d 100644
--- a/app/api/orgs/enrich/route.ts
+++ b/app/api/orgs/enrich/route.ts
@@ -34,7 +34,7 @@ async function extractMission(url: string, orgName: string): Promise<{ mission:
// Use Gemini to extract mission/description
const geminiRes = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`,
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -70,7 +70,7 @@ async function findOrgWebsite(orgName: string, ein?: string): Promise<string | n
// Try a simple Google search via Gemini
const prompt = `What is the official website URL for the nonprofit organization "${orgName}"${ein ? ` (EIN: ${ein})` : ''}? Return ONLY the URL, nothing else. If you don't know, return "unknown".`;
const res = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`,
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -200,7 +200,7 @@ export async function POST(request: NextRequest) {
if (!website && !mission) {
try {
const res = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`,
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
diff --git a/app/api/outreach-pipeline/generate/route.ts b/app/api/outreach-pipeline/generate/route.ts
index 23c476e..cc735ee 100644
--- a/app/api/outreach-pipeline/generate/route.ts
+++ b/app/api/outreach-pipeline/generate/route.ts
@@ -9,7 +9,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[outreach-pipeline/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
/**
diff --git a/app/api/outreach/generate/route.ts b/app/api/outreach/generate/route.ts
index eb25ebe..4c40d9d 100644
--- a/app/api/outreach/generate/route.ts
+++ b/app/api/outreach/generate/route.ts
@@ -10,7 +10,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[outreach/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
/**
diff --git a/app/api/petitions/draft/route.ts b/app/api/petitions/draft/route.ts
index b216fac..bb7d097 100644
--- a/app/api/petitions/draft/route.ts
+++ b/app/api/petitions/draft/route.ts
@@ -10,7 +10,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
export async function POST(req: NextRequest) {
const auth = requireRole(req, 'admin', 'staff');
@@ -84,7 +84,7 @@ Return ONLY valid JSON (no markdown fences) with this exact structure:
talkingPoints: parsed.talkingPoints || [],
suggestedGoal: parsed.suggestedGoal || 10000,
aiConfidence: parsed.confidence || 0.7,
- aiModel: 'gemini-2.0-flash',
+ aiModel: 'gemini-2.5-flash',
sourceTopic: topicTitle,
});
} catch (err) {
diff --git a/app/api/petitions/ideas/route.ts b/app/api/petitions/ideas/route.ts
index 17d1b0b..b2b188a 100644
--- a/app/api/petitions/ideas/route.ts
+++ b/app/api/petitions/ideas/route.ts
@@ -3,7 +3,7 @@ import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
interface PetitionIdea {
title: string;
diff --git a/app/api/pipeline/generate/route.ts b/app/api/pipeline/generate/route.ts
index 856650e..1fd2f91 100644
--- a/app/api/pipeline/generate/route.ts
+++ b/app/api/pipeline/generate/route.ts
@@ -10,7 +10,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[pipeline/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
/**
@@ -136,7 +136,7 @@ export async function POST(request: NextRequest) {
topic_description: data.topic_description || null,
petition_suggestion: data.petition_suggestion || null,
urgency: data.urgency || null,
- model: 'gemini-2.0-flash',
+ model: 'gemini-2.5-flash',
generated_at: new Date().toISOString(),
generation_params: {
paragraphs: data.paragraphs || 4,
@@ -170,7 +170,7 @@ export async function POST(request: NextRequest) {
{
topic_title: data.topic_title,
ai_confidence: confidence,
- model: 'gemini-2.0-flash',
+ model: 'gemini-2.5-flash',
},
ip
);
diff --git a/app/api/pulse/petitions/[id]/sign/route.ts b/app/api/pulse/petitions/[id]/sign/route.ts
index 14848f4..0e387d1 100644
--- a/app/api/pulse/petitions/[id]/sign/route.ts
+++ b/app/api/pulse/petitions/[id]/sign/route.ts
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { createHash } from 'crypto';
import { query, getClient } from '@/lib/db';
+import { checkRateLimit } from '@/lib/rate-limit';
type RouteContext = { params: Promise<{ id: string }> };
@@ -19,6 +20,16 @@ export async function POST(request: NextRequest, context: RouteContext) {
return NextResponse.json({ error: 'Invalid petition ID' }, { status: 400 });
}
+ // Public, unauthenticated write — throttle per client IP to curb
+ // signature-spam floods (20 signs / minute / IP).
+ const rl = checkRateLimit(request, 20, 60_000, 'petition-sign');
+ if (rl.limited) {
+ return NextResponse.json(
+ { error: 'Too many requests. Please slow down.' },
+ { status: 429, headers: { 'Retry-After': String(Math.ceil(rl.resetIn / 1000)) } },
+ );
+ }
+
try {
const body = await request.json();
diff --git a/app/api/scraper/suggestions/route.ts b/app/api/scraper/suggestions/route.ts
index d2b912d..c9475d7 100644
--- a/app/api/scraper/suggestions/route.ts
+++ b/app/api/scraper/suggestions/route.ts
@@ -5,7 +5,7 @@ import { auditLog } from '@/lib/audit';
import { getBrand } from '@/lib/brand';
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`;
/**
* Petition suggestion target map — maps categories to likely petition targets.
diff --git a/app/api/social/ai-compose/route.ts b/app/api/social/ai-compose/route.ts
index ca1666c..6a79703 100644
--- a/app/api/social/ai-compose/route.ts
+++ b/app/api/social/ai-compose/route.ts
@@ -6,7 +6,7 @@ function getGeminiUrl(): string {
if (!GEMINI_API_KEY) {
throw new Error('[social/ai-compose] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`;
}
// Character limits and tone guidance per platform
diff --git a/app/api/statements/generate/route.ts b/app/api/statements/generate/route.ts
index f6276b3..bd10471 100644
--- a/app/api/statements/generate/route.ts
+++ b/app/api/statements/generate/route.ts
@@ -10,7 +10,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[statements/generate] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
/**
@@ -170,7 +170,7 @@ export async function POST(request: NextRequest) {
generated.category || data.category || null,
data.urgency || 'standard',
generated.tags || [],
- 'gemini-2.0-flash',
+ 'gemini-2.5-flash',
confidence,
prompt.slice(0, 2000),
data.news_item_id || null,
diff --git a/app/api/users/route.ts b/app/api/users/route.ts
index 74de29a..9ce0820 100644
--- a/app/api/users/route.ts
+++ b/app/api/users/route.ts
@@ -9,7 +9,9 @@ export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export async function GET(request: NextRequest) {
- const auth = requireRole(request, 'admin', 'staff');
+ // Admin-only: lists all admin/staff/intern accounts (see query below). Staff
+ // must not enumerate other users' identities.
+ const auth = requireRole(request, 'admin');
if (auth instanceof NextResponse) return auth;
const result = await query<{
diff --git a/app/api/webhooks/pulse-topic/route.ts b/app/api/webhooks/pulse-topic/route.ts
index dbaf959..4fff251 100644
--- a/app/api/webhooks/pulse-topic/route.ts
+++ b/app/api/webhooks/pulse-topic/route.ts
@@ -10,7 +10,7 @@ function getGeminiUrl(): string {
if (!geminiApiKey) {
throw new Error('[webhook/pulse-topic] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${geminiApiKey}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiApiKey}`;
}
/**
@@ -151,7 +151,7 @@ export async function POST(request: NextRequest) {
urgency: topic.urgency,
article_count: topic.article_count || null,
avg_sentiment: topic.avg_sentiment || null,
- model: 'gemini-2.0-flash',
+ model: 'gemini-2.5-flash',
generated_at: new Date().toISOString(),
webhook_topic_id: data.topic_id || null,
}),
@@ -299,7 +299,7 @@ GENERATE an official statement. Return valid JSON:
generated.category || null,
'breaking',
generated.tags || [],
- 'gemini-2.0-flash',
+ 'gemini-2.5-flash',
confidence,
],
);
diff --git a/app/api/xsessions/route.ts b/app/api/xsessions/route.ts
index ebc3aa6..4bcf472 100644
--- a/app/api/xsessions/route.ts
+++ b/app/api/xsessions/route.ts
@@ -3,7 +3,7 @@ import { query, getClient } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
/**
* GET /api/xsessions
diff --git a/lib/api-registry.ts b/lib/api-registry.ts
index 033119e..ff48597 100644
--- a/lib/api-registry.ts
+++ b/lib/api-registry.ts
@@ -97,7 +97,7 @@ export const INTEGRATIONS: IntegrationEntry[] = [
required: true,
signupUrl: 'https://aistudio.google.com/apikey',
docsUrl: 'https://ai.google.dev/gemini-api/docs',
- pricing: 'Free tier: 15 RPM / 1M tokens/min / 1.5K req/day on gemini-2.0-flash',
+ pricing: 'Free tier: 15 RPM / 1M tokens/min / 1.5K req/day on gemini-2.5-flash',
getSteps: [
'Visit https://aistudio.google.com/apikey (sign in with the same Google account that owns norma-490521 ideally).',
'Click "Create API Key" → pick the Google Cloud project (norma-490521).',
diff --git a/lib/auth.ts b/lib/auth.ts
index 087e158..be24457 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -131,31 +131,12 @@ export function verifySessionToken(token: string): AuthSession | null {
};
}
- // Old format: base64(username:timestamp:signature) — backward compat
- const decoded = Buffer.from(token, 'base64').toString('utf-8');
- const parts = decoded.split(':');
- if (parts.length !== 3) return null;
-
- const [username, timestamp, signature] = parts;
-
- // Check expiry
- const age = Date.now() - parseInt(timestamp, 10);
- if (isNaN(age) || age > MAX_AGE_SECONDS * 1000) return null;
-
- // Verify signature
- const expected = createHash('sha256')
- .update(`${username}:${timestamp}:${SESSION_SECRET}`)
- .digest('hex');
- try {
- const sigBuffer = Buffer.from(signature, 'hex');
- const expectedBuffer = Buffer.from(expected, 'hex');
- if (sigBuffer.length !== expectedBuffer.length || !timingSafeEqual(sigBuffer, expectedBuffer)) return null;
- } catch {
- return null;
- }
-
- // Old tokens are always admin with no org
- return { username, role: 'admin', orgId: null };
+ // Old format (base64 username:timestamp:signature) is RETIRED. It hardcoded
+ // role:'admin' for ANY valid legacy token — a silent privilege-escalation
+ // risk. Reject it; holders simply re-login and receive a new HMAC-signed,
+ // role-correct token. (Legacy tokens are <=24h expiry, so nearly all are
+ // already dead.)
+ return null;
} catch {
return null;
}
diff --git a/lib/chat.ts b/lib/chat.ts
index 6364ef5..f9f9dfd 100644
--- a/lib/chat.ts
+++ b/lib/chat.ts
@@ -65,7 +65,7 @@ ${draft.body_html}`;
{ role: 'user', parts: [{ text: userMessage }] },
];
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`;
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
diff --git a/lib/detect-mentions.ts b/lib/detect-mentions.ts
index 2c283fe..86b6c11 100644
--- a/lib/detect-mentions.ts
+++ b/lib/detect-mentions.ts
@@ -22,7 +22,7 @@ function getGeminiApiUrl(): string {
if (!GEMINI_API_KEY) {
throw new Error('[detect-mentions] GEMINI_API_KEY env var is required');
}
- return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
+ return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_API_KEY}`;
}
// ---------------------------------------------------------------------------
diff --git a/lib/generator.ts b/lib/generator.ts
index 0fda606..3e3a833 100644
--- a/lib/generator.ts
+++ b/lib/generator.ts
@@ -52,7 +52,7 @@ async function callGemini(prompt: string): Promise<string> {
const key = process.env.GEMINI_API_KEY;
if (!key) throw new Error('GEMINI_API_KEY not configured');
- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`;
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`;
const res = await fetch(url, {
method: 'POST',
diff --git a/lib/ingest-news.ts b/lib/ingest-news.ts
index bb3dc6c..1bf5995 100644
--- a/lib/ingest-news.ts
+++ b/lib/ingest-news.ts
@@ -34,7 +34,7 @@ export const DEFAULT_NEWS_FEEDS: string[] = [
];
// Gemini Flash API for summarization — uses env key (billed)
-const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${process.env.GEMINI_API_KEY || ''}`;
+const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${process.env.GEMINI_API_KEY || ''}`;
// ---------------------------------------------------------------------------
// RSS Parsing
diff --git a/lib/petition-engine.ts b/lib/petition-engine.ts
index f451641..2959b7e 100644
--- a/lib/petition-engine.ts
+++ b/lib/petition-engine.ts
@@ -234,7 +234,7 @@ export async function getTargetLegislators(petitionId: string, limit = 10) {
═══════════════════════════════════════════════════════════════════════════ */
const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
+const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`;
/**
* Generate a personalized legislator email using Gemini AI.
@@ -301,7 +301,7 @@ Return ONLY valid JSON (no markdown fences) with this structure:
bodyHtml: parsed.body_html || `<p>Dear ${legislator.name},</p><p>${constituentCount} constituents support: ${petition.title}</p>`,
bodyText: parsed.body_text || `Dear ${legislator.name}, ${constituentCount} constituents support: ${petition.title}`,
talkingPoints: parsed.talking_points || [],
- aiModel: 'gemini-2.0-flash',
+ aiModel: 'gemini-2.5-flash',
aiConfidence: parsed.confidence || 0.7,
};
} catch (err) {
← e0ef009 security(A-prime): scope login brute-force limiter per (ip,u
·
back to Norma Platform
·
test: add green security-regression suite (18 assertions) + 89bc532 →