[object Object]

← 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

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 →