Skip to content

AI image provenance checker + survivor-security roadmap - #11

Merged
saquibreja7-hash merged 4 commits into
masterfrom
feature/ai-image-checker
Aug 2, 2026
Merged

AI image provenance checker + survivor-security roadmap#11
saquibreja7-hash merged 4 commits into
masterfrom
feature/ai-image-checker

Conversation

@saquibreja7-hash

Copy link
Copy Markdown
Owner

What this adds

A privacy-preserving AI image checker at /check-image, plus the strategic docs for the survivor-security direction.

Checker

  • Client-side reader (c2pa + exifr): reads C2PA Content Credentials and IPTC/EXIF AI tags fully in-browser. No upload.
  • Opt-in deeper check: forwards the image to OpenAI's content_provenance_checks endpoint (C2PA + SynthID watermark) through a new in-memory server route (/api/check-image) that stores nothing. Guarded by CSRF, IP rate limit, adults-only gate, and explicit consent.
  • Detection never gates help; results are shown as facts, not verdicts. Honest caveats throughout (EN + HI).

Docs

  • docs/product/survivor-security-roadmap.md — the response-centre roadmap.
  • docs/adr/002-consented-encrypted-media.md — the 'consented, encrypted, time-limited media' charter change (proposed, pending legal), with the documented OpenAI third-party exception.

Also

  • Cloudflare for Startups credit badge on the homepage.

Config

  • Needs OPENAI_API_KEY set locally and in Vercel for the deeper check. If unset, the deeper check returns 503 and the on-device reader still works.

Verification

  • type-check clean, lint clean (pre-existing warnings only), no-fetch safety tests pass.
  • End-to-end tested against the live OpenAI endpoint: valid response, correct schema, gates enforced.

🤖 Generated with Claude Code

Adds /check-image: an adults-gated, privacy-preserving tool to check whether
an image carries AI-origin signals.

- Client-side reader (c2pa + exifr) reads C2PA Content Credentials and
  IPTC/EXIF tags fully in-browser; no upload.
- Optional opt-in deeper check forwards the image to OpenAI's
  content_provenance_checks endpoint (C2PA + SynthID watermark) via a new
  in-memory server route that stores nothing. Guarded by CSRF, IP rate limit,
  adults-only gate, and explicit consent.
- Detection never gates help; results are shown as facts, never verdicts.
- Cloudflare for Startups credit badge on the homepage.
- Product roadmap (survivor-security direction) + ADR 002 (consented,
  encrypted, time-limited media) with the OpenAI third-party exception.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
meri-asmita Ready Ready Preview Aug 2, 2026 1:21pm


const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "provider_unconfigured" }, { status: 503 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: This route (and the whole /api/check-image deep-check feature) forwards user-submitted images to OpenAI from the server, which is exactly the capability ADR-002 says must not ship until Indian counsel, a security review, and an explicit child-protection routing protocol are in place. The only gate here is whether OPENAI_API_KEY happens to be set (line 73-76) — that's a deploy-configuration accident, not a safety gate. There is no analogous ENABLE_* flag (compare ENABLE_HASH_UPLOAD/ENABLE_PLATFORM_API) that defaults off in production. Since Vercel preview is documented as production-equivalent with full env vars set, adding OPENAI_API_KEY to .env.example/Vercel env (as this PR does) will make this feature live the moment it's configured — before the ADR's own blockers are cleared. Please add an explicit env-var gate (e.g. ENABLE_AI_PROVENANCE_DEEP_CHECK=true) that stays off in all environments until ADR-002 is accepted, per the ADR's own 'Status and blockers' section.

// Both gates are mandatory: adults-only, and explicit consent to send to OpenAI.
if (form.get("ageConfirmed") !== "true") {
logSecurityEvent({ event: "minor_route_blocked", route: "/api/check-image", reason: "age_not_confirmed" });
return NextResponse.json({ error: "age_not_confirmed" }, { status: 400 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: The 'adults-only' gate is pure client self-attestation — the server only checks that the form field ageConfirmed equals the literal string "true", and the client (ImageChecker.tsx) always sends that literal regardless of any real verification. Since this endpoint is public and unauthenticated, anyone can curl it directly with ageConfirmed=true&consent=true and any image bytes, and the server will relay that image straight to OpenAI with zero technical safeguard. Given this tool is explicitly aimed at NCII images (per the roadmap), there's a real risk of the server forwarding an image of a minor to a third party — exactly the scenario ADR-002 says needs 'an explicit protocol' before shipping, and exactly what 'minors never reach the case flow' is meant to prevent for the rest of the app. A checkbox with no server-side verification is not a minor-exclusion gate.

Comment thread asmita/src/lib/ai-provenance.ts Outdated

const C2PA_VERSION = "0.30.17";
const C2PA_WASM = `https://cdn.jsdelivr.net/npm/c2pa@${C2PA_VERSION}/dist/assets/wasm/toolkit_bg.wasm`;
const C2PA_WORKER = `https://cdn.jsdelivr.net/npm/c2pa@${C2PA_VERSION}/dist/c2pa.worker.min.js`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The client-side reader loads executable WASM and a Web Worker script from a third-party CDN (jsdelivr) with no subresource-integrity check: createC2pa({ wasmSrc: C2PA_WASM, workerSrc: C2PA_WORKER }). If jsdelivr or the c2pa npm package were ever compromised, that code would execute in a survivor's browser on this origin. Worth noting the c2pa package itself is also deprecated upstream ('use @contentauth/c2pa-web instead', per the lockfile). Consider self-hosting these assets or adding SRI hashes, and tracking migration off the deprecated package.

Comment thread asmita/src/app/api/check-image/route.ts Outdated
}

const ip = getClientIp(request);
const limit = checkRateLimit(`check-image:${sha256(ip)}`, 10, 60 * 60 * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This per-IP rate limit (10/hour) is the only abuse/cost control on a route that makes a paid third-party API call and forwards user content externally. checkRateLimit falls back to the in-memory Map unless RATE_LIMIT_DRIVER=redis is configured, and this codebase's own architecture notes that 'Vercel serverless functions are stateless' — so on Vercel this limit likely doesn't hold across function instances in production. Worth confirming RATE_LIMIT_DRIVER=redis is actually set wherever this route is deployed, otherwise the limit is effectively decorative for this specific endpoint.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review summary

This PR adds a client-side AI-provenance/C2PA reader plus an optional server-side "deeper check" that forwards the uploaded image to OpenAI's content-provenance endpoint. It also adds docs/adr/002-consented-encrypted-media.md, which explicitly reopens the platform's "no media, ever" charter and states this ADR is proposed, not accepted and must not ship until legal counsel, a security review, and a child-protection routing protocol are in place.

The client-side reader (ai-provenance.ts, exifr/C2PA in-browser) is fine — it never leaves the device. The concern is the server route.

Critical issues (2):

  • src/app/api/check-image/route.ts ships the OpenAI-forwarding behavior with no real feature flag — the only "gate" is whether OPENAI_API_KEY happens to be set, which is a deploy-configuration detail, not a safety gate like the existing ENABLE_HASH_UPLOAD/ENABLE_PLATFORM_API pattern. Since Vercel preview is documented as production-equivalent, setting the key (which .env.example now invites) makes this feature live immediately — contradicting the ADR's own "must not ship until" language in the same PR.
  • The adults-only gate on that route is pure client self-attestation (form.get("ageConfirmed") !== "true"), with no server-side verification. Because the endpoint is public and unauthenticated, anyone can bypass the UI entirely and have the server relay arbitrary image content — including potentially images of minors — straight to a third party.

Warnings (2): no subresource-integrity on the CDN-loaded WASM/worker (and the c2pa package is deprecated upstream); the per-IP rate limit relies on in-memory state that likely doesn't hold across stateless Vercel serverless instances, so it may not effectively cap either cost or abuse in production.

Given the core "no media, ever" safety principle and the PR's own ADR blockers, I'd hold this until the OpenAI forwarding path is behind a real off-by-default flag and gated by something stronger than a checkbox.

Verdict: BLOCK

Addresses the security review on PR #11:
- /api/check-image returns 404 unless ENABLE_PROVENANCE_CHECK=true, so setting
  OPENAI_API_KEY alone never makes the third-party forwarding live. Flag stays
  off in production/preview until ADR 002 legal blockers clear.
- Deeper-check UI is hidden unless the flag is on.
- Documented the client-attested age-gate limitation and the serverless
  rate-limit caveat in ADR 002 as blockers before the flag can be enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread asmita/src/lib/ai-provenance.ts Outdated
// itself: it does not guess, and it cannot falsely accuse a genuine photo.

const C2PA_VERSION = "0.30.17";
const C2PA_WASM = `https://cdn.jsdelivr.net/npm/c2pa@${C2PA_VERSION}/dist/assets/wasm/toolkit_bg.wasm`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: The app's CSP (asmita/next.config.ts, script-src 'self', connect-src 'self') was not updated by this PR and does not allow cdn.jsdelivr.net. That CSP applies unconditionally (not gated on NODE_ENV) to every route including /check-image, so the browser will block both the fetch for toolkit_bg.wasm (connect-src) and the c2pa.worker.min.js worker script (script-src/worker-src fallback) in every environment — dev, preview, and prod alike. Because readContentCredentials() swallows all failures in a bare try/catch ("A missing or unreadable manifest is the common case; not an error to the user"), this isn't surfaced anywhere: survivors will always see "No AI credential found" even though the C2PA check never actually ran. That's a silent failure of a feature this exact page markets as working ("What we found"). Needs either a CSP exception for the jsdelivr origin (script-src/worker-src/connect-src) or, better, self-hosting the wasm/worker assets under /self so no CSP change is needed.

Comment thread asmita/src/lib/ai-provenance.ts Outdated
async function readContentCredentials(file: File, result: ProvenanceResult): Promise<void> {
try {
const { createC2pa } = await import("c2pa");
const c2pa = await createC2pa({ wasmSrc: C2PA_WASM, workerSrc: C2PA_WORKER });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Separately from the CSP block above — even once jsdelivr is allow-listed, this loads executable WASM + a Web Worker script from a public CDN at runtime with no Subresource Integrity (no integrity hash passed anywhere). For a tool whose entire stated promise on this page is that the image "never leaves the device," that promise now transitively depends on jsdelivr and this specific npm publish never being compromised — a compromised worker has direct access to the raw image bytes client-side and could exfiltrate them, which would be a silent, undetectable violation of the "no media, ever" principle. Recommend self-hosting toolkit_bg.wasm and c2pa.worker.min.js from the app's own origin (also fixes the CSP issue above for free) or adding SRI if self-hosting isn't feasible.

Comment thread asmita/src/app/api/check-image/route.ts Outdated
}

const ip = getClientIp(request);
const limit = checkRateLimit(`check-image:${sha256(ip)}`, 10, 60 * 60 * 1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This is the only API route in the codebase using the synchronous, memory-only checkRateLimit — every other route (platforms/detect, cases/[caseId]/urls, cases/[caseId]/hashes, auth/request-otp, admin/auth/request-otp) uses checkRateLimitAsync. Per src/lib/rate-limit.ts, checkRateLimit throws synchronously ("Use checkRateLimitAsync when RATE_LIMIT_DRIVER=redis") whenever RATE_LIMIT_DRIVER=redis + REDIS_URL are set — which is the documented production driver. That throw isn't caught here, so if this route is ever enabled in an environment with the Redis rate-limit driver configured, every request 500s outright rather than degrading to the "doesn't hold across serverless instances" limitation already called out in ADR 002. Should be await checkRateLimitAsync(...) like the rest of the routes.

Comment thread asmita/package.json Outdated
"@types/qrcode": "^1.5.6",
"@vercel/analytics": "^2.0.1",
"bullmq": "^5.76.7",
"c2pa": "^0.30.17",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: package-lock.json shows this pulls in c2pa@0.30.17, which is deprecated upstream ("This package is no longer being actively developed. Please use @contentauth/c2pa-web instead."). It's the library doing content-authenticity parsing of arbitrary, attacker-influenced image files client-side — worth moving to the maintained @contentauth/c2pa-web before this ships broadly, since it won't get further security fixes.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review summary

This PR adds the client-side C2PA/EXIF provenance reader (ai-provenance.ts, /check-image) and the off-by-default OpenAI "deeper check" API route, plus ADR 002 and the roadmap doc. The dispatch-gate discipline here is good: /api/check-image 404s unless ENABLE_PROVENANCE_CHECK=true, both age-attestation and explicit per-image OpenAI-consent are required server-side, CSRF + rate limiting + file type/size checks are all present, and the route genuinely never writes the image to disk/db/logs — consistent with ADR 002's documented exception.

Found one CRITICAL and three WARNINGs (posted inline):

  1. CRITICAL — The app's CSP (next.config.ts, unchanged by this PR: script-src 'self', connect-src 'self') has no exception for cdn.jsdelivr.net, so the browser will block both the wasm fetch and the worker script that ai-provenance.ts loads from jsdelivr, in every environment. Because the failure is swallowed by a bare try/catch, the client-side C2PA check silently never runs, and survivors always see "No AI credential found" regardless of the actual file — a silent failure of the feature this page markets as working.
  2. WARNING — Same CDN dependency, separate issue: no Subresource Integrity on the wasm/worker fetched from jsdelivr. For a tool whose whole promise is "the image never leaves your device," that guarantee now depends on jsdelivr/npm supply-chain integrity with no pinning beyond the version string. Self-hosting the assets fixes both this and Test: CI + AI review pipeline #1.
  3. WARNING/api/check-image uses the synchronous, memory-only checkRateLimit instead of checkRateLimitAsync, which is what every other route in the codebase uses. If RATE_LIMIT_DRIVER=redis is ever set alongside this flag, the sync call throws uncaught and the route 500s instead of degrading.
  4. WARNING — The new c2pa npm dependency is deprecated upstream in favor of @contentauth/c2pa-web.

No issues found with the OpenAI dispatch path itself, the dev bypass flags, PII handling, or the minor-routing link — those all look correctly scoped and gated.

Addresses the security review's CSP critical: the app CSP (script-src/connect-src
'self') blocked the c2pa wasm+worker loaded from jsdelivr, so the on-device
Content Credentials read silently never ran. Removes the c2pa browser dependency
entirely and relies on exifr (pure JS, no CDN, no WASM) to read IPTC/XMP/EXIF
AI-origin tags on device. Full C2PA + SynthID verification remains available
server-side via the OpenAI deeper check.

Also resolves review warnings: removes the deprecated c2pa package and the
CDN supply-chain/SRI concern, and switches /api/check-image to checkRateLimitAsync
for consistency with every other route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
// Note: age is client self-attested and cannot be verified on a public
// endpoint. When this flag is eventually enabled, the route must additionally
// be bound to an authenticated case that already passed the adult age gate.
if (process.env.ENABLE_PROVENANCE_CHECK !== "true") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The adults-only gate for this route is pure client self-attestation (form.get("ageConfirmed") === "true") on an unauthenticated public endpoint — any direct caller (curl, script) can set this to true regardless of the actual image content, and there is no server-side verification tying the request to an age-verified case. Per CLAUDE.md, "Minors never reach the case flow" is a hard invariant, and here the consequence of a bypass is worse than usual: an image is forwarded to a third party (OpenAI) if ENABLE_PROVENANCE_CHECK is ever turned on. The PR's own ADR 002 flags this as a 'known limitation to resolve before enabling,' which is good, but right now the only control preventing exploitation is the env-var gate + a documentation promise not to set it in prod/preview. Given the severity if that promise is ever broken (e.g. someone flips the flag without reading the ADR), consider a technical safeguard beyond the single env var — e.g. also require a second independent flag, or refuse to call the OpenAI branch unless running with NODE_ENV !== "production", so a single misconfigured env var in Vercel can't turn this on in a live environment.


// Both gates are mandatory: adults-only, and explicit consent to send to OpenAI.
if (form.get("ageConfirmed") !== "true") {
logSecurityEvent({ event: "minor_route_blocked", route: "/api/check-image", reason: "age_not_confirmed" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: this reuses the minor_route_blocked security event, which elsewhere (src/app/api/cases/create/route.ts) means a real POCSO/minor-pathway routing decision — a legally significant signal. Here it fires for anyone who simply leaves an unchecked checkbox on an anonymous, unauthenticated tool with no actual minor detection behind it. Mixing high-signal minor-routing events with high-noise 'forgot to tick a box' events in the same audit/alerting bucket risks diluting monitoring built around the POCSO obligation. Consider a distinct event type (e.g. check_image_age_not_confirmed) so the audit trail for genuine minor-routing stays meaningful.

return NextResponse.json({ error: "provider_error" }, { status: 502 });
}

const payload = (await openaiResponse.json()) as { results?: OpenAiResult[] };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: the OpenAI response body is trusted without schema validation — payload.results, r.outcome, etc. are read directly as the declared OpenAiResult type with no runtime check (no Zod), even though the codebase's own lint rule elsewhere insists on validating external payloads before reading fields. If OpenAI changes/renames a field or returns an unexpected shape, detected silently resolves to false for every check (since r.outcome === "detected" just fails to match) and survivors are shown 'no signal found' instead of an error. Since this never gates help it isn't safety-critical, but it can quietly produce a wrong informational result. Worth validating the upstream shape and surfacing a provider_error if it doesn't match, rather than defaulting to a negative result.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review: PR #11 — AI image provenance checker

Reviewed the new client-side EXIF/C2PA reader (ai-provenance.ts, /check-image page) and the OpenAI-backed "deeper check" route (/api/check-image), plus the accompanying ADR 002 and roadmap docs.

No CRITICAL findings. The design respects the core safety invariants well:

  • The primary /check-image reader is 100% client-side (exifr, no network, no WASM/CDN) — no media ever reaches the server for that path, consistent with "no media, ever."
  • The new server route that does forward images to a third party (OpenAI) is gated behind ENABLE_PROVENANCE_CHECK, off by default, returning a plain 404 when unset — matching the codebase's existing off-by-default flag pattern (ENABLE_HASH_UPLOAD, DEV_SKIP_LEGAL_REVIEW). .env.example ships blank, and the exception is explicitly documented and risk-accepted in ADR 002 (proposed, not accepted, blocked on legal sign-off).
  • CSRF, per-IP rate limiting, file-type/size checks, and no on-disk/log persistence of the image are all present. No secrets or real PII in code, tests, or docs.
  • The "no submitted-URL fetch" lint invariant (eslint.config.mjs) doesn't apply here since the route fetches a hardcoded OpenAI endpoint, not a survivor-submitted URL — consistent with how that rule is scoped.

Three WARNINGs posted inline for the author's judgment (none block merge, but worth tracking before ENABLE_PROVENANCE_CHECK is ever turned on anywhere real):

  1. route.ts:42 — the adults-only gate is pure client self-attestation on a public, unauthenticated endpoint; the only real control against a bypass is the env-var gate + a documentation promise. Given the severity if OpenAI ever receives imagery of a minor, consider a technical safeguard beyond a single env var (already flagged as a pre-enable blocker in ADR 002, but worth a stronger runtime check too).
  2. route.ts:67 — reuses the minor_route_blocked security-event type that elsewhere means a real POCSO/minor-pathway decision (api/cases/create/route.ts), which risks diluting audit signal for that legally significant event with noise from an anonymous tool's unchecked checkbox.
  3. route.ts:110 — the OpenAI response shape is trusted with no runtime validation; a contract change upstream would silently downgrade every result to "not detected" rather than surfacing an error. Low severity since results never gate help, but worth a shape check.

Verdict: PASS

const csrf = await (await fetch("/api/csrf", { cache: "no-store" })).json();
const body = new FormData();
body.append("file", file);
body.append("ageConfirmed", "true");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: runDeepCheck always sends body.append("ageConfirmed", "true") regardless of the actual ageConfirmed" state, so the client never actually forwards the real value of the age checkbox. Currently harmless in effect (the route is off by default via ENABLE_PROVENANCE_CHECK, and the field is unauthenticated client-supplied data anyway per ADR 002's own documented caveat), but it means even a well-behaved browser client can't meaningfully signal 'not confirmed' to the server once a file has been selected — this should read body.append("ageConfirmed", String(ageConfirmed))` so the field isn't permanently dead/misleading code once the flag is eventually turned on.

<section className="container py-14 md:py-20">
<ImageChecker locale={locale} deepCheckEnabled={process.env.ENABLE_PROVENANCE_CHECK === "true"} />
<p className="muted mx-auto mt-8 max-w-xl text-center text-xs leading-[1.7]">
{t(locale, "check.privacy.note")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: This privacy note ("the file you choose is never sent to Asmita or anyone else") is rendered unconditionally directly under the checker, including the space where the deeper-check subsection appears when ENABLE_PROVENANCE_CHECK=true. Once that flag is ever turned on, this blanket claim becomes false for any survivor who opts into the deeper check (the file is sent to OpenAI). Consider scoping this note to the on-device reader only, or making it conditional on deepCheckEnabled, so the copy can't drift out of sync with the flag.

@claude

claude Bot commented Aug 2, 2026

Copy link
Copy Markdown

Safety/security review — PR #11 (AI image provenance checker)

Reviewed the cumulative diff (3 commits) that adds the public /check-image tool: a client-side C2PA/EXIF reader plus an opt-in "deeper check" that forwards the image to OpenAI's content_provenance_checks endpoint via a new server route.

This PR already incorporates fixes from a prior review round (visible in the commit history): the OpenAI-forwarding route is now gated behind an off-by-default ENABLE_PROVENANCE_CHECK flag (returns 404 unless explicitly set — checked before CSRF/rate-limit/anything else runs), the browser-side C2PA/WASM dependency loaded from a third-party CDN (jsdelivr) was dropped entirely in favor of a pure-JS, no-network exifr read, and the rate limiter was switched to the async/Redis-capable driver for consistency with the rest of the app. ENABLE_PROVENANCE_CHECK and OPENAI_API_KEY are not referenced anywhere else in the repo (no vercel.json/CI wiring), so nothing in this PR makes third-party media forwarding live today.

Checked against the core safety invariants:

  • No media reaches the server in the default (shipped) configuration — the on-device reader (ai-provenance.ts) never uploads the file, and the only path that would is fully disabled by default.
  • No real NCII URLs, PII, or Aadhaar data introduced in code/docs/i18n.
  • CSRF + IP rate limiting present and consistent with other routes; no new auth/authz surface (route is intentionally public, like /how-it-works).
  • No changes to case lifecycle, notice dispatch, hash dispatch, or audit chain.
  • ADR 002 explicitly documents this as a narrow, reviewed, currently-blocked exception to "no media, ever," with its own known-limitations list (client self-attested age gate, in-memory rate limiter not surviving serverless restarts) called out as blockers before the flag can ever be turned on for real.

Two non-blocking findings posted inline (both WARNING, left inline on the PR):

  1. ImageChecker.tsxrunDeepCheck() hardcodes ageConfirmed: "true" in the request body instead of forwarding the actual checkbox state, making that field permanently dead/misleading (doesn't currently matter since the route is 404'd by default and the field is unauthenticated anyway, but should be fixed before anyone relies on it).
  2. check-image/page.tsx — the static privacy note ("never sent to Asmita or anyone else") is unconditional and will become inaccurate the moment ENABLE_PROVENANCE_CHECK is ever turned on and a survivor opts into the deeper check.

No exploitable issues, safety-invariant breaks, or data-integrity problems found in the code as it will actually run with current configuration.

Verdict: PASS

@saquibreja7-hash
saquibreja7-hash merged commit 33890ed into master Aug 2, 2026
8 checks passed
@saquibreja7-hash
saquibreja7-hash deleted the feature/ai-image-checker branch August 2, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant