diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index c7fd9e7..5a5d0f8 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -105,3 +105,20 @@ jobs: - name: Deploy run: npx wrangler deploy --compatibility-date 2024-01-01 continue-on-error: true + + deploy-authichain-automation: + name: Deploy authichain-automation + runs-on: ubuntu-latest + defaults: + run: + working-directory: workers/authichain-automation + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install Wrangler + run: npm install --no-save wrangler || npm install -g wrangler + - name: Deploy + run: npx wrangler deploy + continue-on-error: true diff --git a/app/agent-browser/page.tsx b/app/agent-browser/page.tsx index 40e5f03..6d7d693 100644 --- a/app/agent-browser/page.tsx +++ b/app/agent-browser/page.tsx @@ -39,7 +39,7 @@ function AgentBrowserInner() { const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') useEffect(() => { - const planParam = searchParams.get('plan') + const planParam = searchParams?.get('plan') if (planParam === 'pro' || planParam === 'enterprise') { setPlan(planParam) } diff --git a/app/api/characters/generate/route.ts b/app/api/characters/generate/route.ts new file mode 100644 index 0000000..d4d6300 --- /dev/null +++ b/app/api/characters/generate/route.ts @@ -0,0 +1,127 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; +import { buildOpenArtPrompt } from '@/packages/characters/src/prompt'; +import { OpenArtClient } from '@/packages/openart/src/client'; + +function scoreVariant(index: number) { + const seeded = [ + { protocol: 9.2, thumb: 8.8, premium: 9.1, silhouette: 9.0, trust: 9.4, mint: 9.0, ui: 8.9 }, + { protocol: 8.7, thumb: 9.1, premium: 8.8, silhouette: 9.3, trust: 8.9, mint: 8.6, ui: 9.2 }, + { protocol: 8.9, thumb: 8.5, premium: 9.4, silhouette: 8.6, trust: 9.0, mint: 9.3, ui: 8.7 }, + { protocol: 8.4, thumb: 8.9, premium: 8.7, silhouette: 8.8, trust: 8.8, mint: 8.5, ui: 9.0 } + ]; + return seeded[index] ?? seeded[0]; +} + +export async function POST(req: NextRequest) { + const supabase = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + + const body = await req.json(); + const { tenant_id, user_id, object_id, archetype, colorway, mood, object_context, brand_context, style } = body; + + if (!archetype) { + return NextResponse.json({ error: 'archetype is required' }, { status: 400 }); + } + + const { prompt, negativePrompt } = buildOpenArtPrompt({ + archetype, + colorway, + mood, + objectContext: object_context, + brandContext: brand_context, + style + }); + + const { data: generation, error: genError } = await supabase + .from('character_generations') + .insert({ + tenant_id, + user_id, + object_id, + archetype, + style: style ?? 'premium futuristic heraldic concept art', + colorway, + mood, + prompt, + negative_prompt: negativePrompt, + provider: 'openart', + provider_model: process.env.OPENART_MODEL ?? 'openart-default', + status: 'pending', + variant_count: 4, + request_payload: body + }) + .select('*') + .single(); + + if (genError || !generation) { + return NextResponse.json({ error: genError?.message ?? 'failed to create generation' }, { status: 500 }); + } + + try { + const client = new OpenArtClient(process.env.OPENART_API_KEY!, process.env.OPENART_BASE_URL); + const generated = await client.generate({ + prompt, + negativePrompt, + numImages: 4, + size: '1024x1536', + transparentBackground: false, + model: process.env.OPENART_MODEL + }); + + const assetsPayload = generated.assets.map((asset, index) => { + const s = scoreVariant(index); + return { + generation_id: generation.id, + tenant_id, + user_id, + provider_asset_id: asset.id, + image_url: asset.imageUrl, + preview_url: asset.previewUrl, + prompt, + metadata: asset.metadata ?? {}, + protocol_fit_score: s.protocol, + thumbnail_clarity_score: s.thumb, + premium_feel_score: s.premium, + silhouette_score: s.silhouette, + trust_symbolism_score: s.trust, + mint_readiness_score: s.mint, + ui_compatibility_score: s.ui + }; + }); + + const { data: insertedAssets, error: assetError } = await supabase + .from('character_assets') + .insert(assetsPayload) + .select('*'); + + if (assetError || !insertedAssets) throw assetError ?? new Error('asset insert failed'); + + const recommended = [...insertedAssets].sort((a, b) => Number(b.total_score) - Number(a.total_score))[0]; + + await supabase.from('character_assets').update({ recommended: true }).eq('id', recommended.id); + await supabase + .from('character_generations') + .update({ + status: 'completed', + response_payload: generated.raw, + best_asset_id: recommended.id + }) + .eq('id', generation.id); + + return NextResponse.json({ + generation_id: generation.id, + best_asset_id: recommended.id, + assets: insertedAssets.map((a) => ({ ...a, recommended: a.id === recommended.id })) + }); + } catch (error: any) { + await supabase + .from('character_generations') + .update({ status: 'failed', response_payload: { error: error?.message ?? 'unknown error' } }) + .eq('id', generation.id); + + return NextResponse.json({ error: error?.message ?? 'generation failed' }, { status: 500 }); + } +} diff --git a/app/api/characters/select/route.ts b/app/api/characters/select/route.ts new file mode 100644 index 0000000..5bcd1cb --- /dev/null +++ b/app/api/characters/select/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@supabase/supabase-js'; + +export async function POST(req: NextRequest) { + const supabase = createClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); + + const body = await req.json(); + const { generation_id, asset_id } = body; + + if (!generation_id || !asset_id) { + return NextResponse.json({ error: 'generation_id and asset_id are required' }, { status: 400 }); + } + + const { data: asset, error: assetError } = await supabase + .from('character_assets') + .select('*') + .eq('id', asset_id) + .eq('generation_id', generation_id) + .single(); + + if (assetError || !asset) { + return NextResponse.json({ error: 'asset not found for generation' }, { status: 404 }); + } + + await supabase.from('character_assets').update({ selected: false }).eq('generation_id', generation_id); + await supabase + .from('character_assets') + .update({ selected: true, selected_at: new Date().toISOString() }) + .eq('id', asset_id); + + const { error: genUpdateError } = await supabase + .from('character_generations') + .update({ status: 'selected', selected_asset_id: asset_id }) + .eq('id', generation_id); + + if (genUpdateError) { + return NextResponse.json({ error: genUpdateError.message }, { status: 500 }); + } + + return NextResponse.json({ + generation_id, + selected_asset_id: asset_id, + status: 'selected' + }); +} diff --git a/app/api/checkout/qron-stake/route.ts b/app/api/checkout/qron-stake/route.ts index b02a87d..6bc8873 100644 --- a/app/api/checkout/qron-stake/route.ts +++ b/app/api/checkout/qron-stake/route.ts @@ -19,14 +19,14 @@ import { createClient as createServerClient } from '@/lib/supabase/server' export const dynamic = 'force-dynamic' -export const QRON_STAKE_BUNDLES = { +const QRON_STAKE_BUNDLES = { bronze: { qron_amount: 1_000, price_usd_cents: 4900, label: '1,000 QRON — Bronze Tier' }, silver: { qron_amount: 10_000, price_usd_cents: 34900, label: '10,000 QRON — Silver Tier' }, gold: { qron_amount: 100_000, price_usd_cents: 249900, label: '100,000 QRON — Gold Tier' }, platinum: { qron_amount: 1_000_000, price_usd_cents: 1499900, label: '1,000,000 QRON — Platinum Tier' }, } as const -export type StakingBundle = keyof typeof QRON_STAKE_BUNDLES +type StakingBundle = keyof typeof QRON_STAKE_BUNDLES export async function POST(req: NextRequest) { const stripeKey = process.env.STRIPE_SECRET_KEY diff --git a/app/api/nft/metadata/[truemarkId]/route.ts b/app/api/nft/metadata/[truemarkId]/route.ts index 376bb7f..8b8c3f6 100644 --- a/app/api/nft/metadata/[truemarkId]/route.ts +++ b/app/api/nft/metadata/[truemarkId]/route.ts @@ -10,9 +10,9 @@ export const dynamic = 'force-dynamic' export async function GET( _req: NextRequest, - { params }: { params: { truemarkId: string } } + { params }: { params: Promise<{ truemarkId: string }> } ) { - const { truemarkId } = params + const { truemarkId } = await params const supabase = createServiceClient() const { data: product } = await supabase diff --git a/app/api/products/[id]/route.ts b/app/api/products/[id]/route.ts index 3164da1..731257a 100644 --- a/app/api/products/[id]/route.ts +++ b/app/api/products/[id]/route.ts @@ -4,8 +4,9 @@ import { normalizeProductRecord } from '@/lib/contracts/products' export async function GET( _request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { + const { id } = await params try { const supabase = await createClient() @@ -21,7 +22,7 @@ export async function GET( const { data: product, error } = await supabase .from('products') .select('*') - .eq('id', params.id) + .eq('id', id) .eq('user_id', user.id) .single() diff --git a/app/api/qron/generate/route.ts b/app/api/qron/generate/route.ts new file mode 100644 index 0000000..17e3a4a --- /dev/null +++ b/app/api/qron/generate/route.ts @@ -0,0 +1,269 @@ +/** + * app/api/qron/generate/route.ts + * + * Full generation pipeline: + * 1. Auth via Supabase session + * 2. Credit check / deduction + * 3. Call fal-ai/illusion-diffusion directly via @fal-ai/client + * 4. Fire /api/qron/register (Supabase row + AuthiChain D1 cross-reg) + * 5. Return imageUrl + registration IDs + * + * Replaces the Supabase Edge Function proxy — fal.ai is called server-side + * so FAL_KEY never touches the client. + */ + +import { NextResponse } from "next/server"; +import { fal } from "@fal-ai/client"; +import { createClient } from "@/utils/supabase/server"; +import { createClient as createAdmin } from "@supabase/supabase-js"; +import crypto from "crypto"; + +export const maxDuration = 60; +export const dynamic = "force-dynamic"; + +// ─── fal.ai client config ──────────────────────────────────────────────────── +fal.config({ credentials: process.env.FAL_KEY! }); + +// ─── Types ──────────────────────────────────────────────────────────────────── +interface GenerateBody { + url?: string; + prompt?: string; + presetId?: string; + mode?: string; + negative_prompt?: string; + guidance_scale?: number; + controlnet_conditioning_scale?: number; + num_inference_steps?: number; + scheduler?: "Euler" | "DPM++ Karras SDE"; + image_size?: "square_hd" | "square" | "portrait_4_3" | "portrait_16_9" | "landscape_4_3" | "landscape_16_9"; + seed?: number; + image_url?: string; +} + +interface FalOutput { + image: { url: string; width: number; height: number; content_type: string }; + seed: number; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function adminClient() { + return createAdmin( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); +} + +function sha256(input: string): string { + return crypto.createHash("sha256").update(input).digest("hex"); +} + +function qrPatternUrl(payloadUrl: string): string { + const encoded = encodeURIComponent(payloadUrl); + return `https://api.qrserver.com/v1/create-qr-code/?size=512x512&ecc=H&data=${encoded}`; +} + +// ─── Credit check ───────────────────────────────────────────────────────────── + +async function checkAndDeductCredit(userId: string): Promise { + const admin = adminClient(); + const { data: profile } = await admin + .from("profiles") + .select("credits, plan") + .eq("id", userId) + .single(); + + if (!profile) return false; + if (profile.plan === "business") return true; + if ((profile.credits ?? 0) < 1) return false; + + const { error } = await admin + .from("profiles") + .update({ credits: profile.credits - 1 }) + .eq("id", userId); + + return !error; +} + +// ─── Registration ───────────────────────────────────────────────────────────── + +async function registerQron(params: { + userId: string; + assetUrl: string; + payloadUrl: string; + prompt: string; + seed: number; +}): Promise<{ id: string | null; authichain_record_id: string | null }> { + const baseUrl = + process.env.NEXT_PUBLIC_APP_URL ?? "https://www.authichain.com"; + const payloadHash = sha256(params.payloadUrl); + + try { + const res = await fetch(`${baseUrl}/api/qron/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.AUTHICHAIN_API_SECRET}`, + }, + body: JSON.stringify({ + user_id: params.userId, + asset_url: params.assetUrl, + payload_hash: payloadHash, + payload_preview: params.payloadUrl.slice(0, 100), + prompt: params.prompt, + seed: params.seed, + chain: "polygon", + status: "pending_mint", + registered_at: new Date().toISOString(), + source: "qron-generate-direct", + }), + signal: AbortSignal.timeout(10_000), + }); + + if (!res.ok) { + console.warn("[qron/generate] register returned", res.status); + return { id: null, authichain_record_id: null }; + } + + const data = (await res.json()) as { + id?: string; + authichain_record_id?: string; + }; + return { + id: data.id ?? null, + authichain_record_id: data.authichain_record_id ?? null, + }; + } catch (err) { + console.warn("[qron/generate] register unreachable:", err); + return { id: null, authichain_record_id: null }; + } +} + +// ─── POST handler ───────────────────────────────────────────────────────────── + +export async function POST(request: Request) { + const supabase = await createClient(); + const { + data: { session }, + } = await supabase.auth.getSession(); + + if (!session) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const userId = session.user.id; + + let body: GenerateBody = {}; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const payloadUrl = body.url?.trim(); + const prompt = body.prompt?.trim(); + + if (!payloadUrl) { + return NextResponse.json( + { error: "url (QR payload) is required" }, + { status: 400 } + ); + } + if (!prompt && !body.presetId) { + return NextResponse.json( + { error: "prompt or presetId is required" }, + { status: 400 } + ); + } + + const hasCredit = await checkAndDeductCredit(userId); + if (!hasCredit) { + return NextResponse.json( + { + error: "Credit limit reached. Upgrade your plan to continue.", + code: "LIMIT_REACHED", + }, + { status: 403 } + ); + } + + const imageUrl = body.image_url ?? qrPatternUrl(payloadUrl); + + let falResult: FalOutput; + try { + const result = await fal.subscribe("fal-ai/illusion-diffusion", { + input: { + image_url: imageUrl, + prompt: prompt ?? "(masterpiece:1.4), (best quality), (detailed), vibrant living art QR portal", + negative_prompt: + body.negative_prompt ?? + "(worst quality, poor details:1.4), lowres, watermark, signature", + guidance_scale: body.guidance_scale ?? 7.5, + controlnet_conditioning_scale: + body.controlnet_conditioning_scale ?? 1.0, + control_guidance_start: 0, + control_guidance_end: 1, + scheduler: body.scheduler ?? "Euler", + num_inference_steps: body.num_inference_steps ?? 40, + image_size: body.image_size ?? "square_hd", + ...(body.seed !== undefined && { seed: body.seed }), + }, + logs: false, + }); + + falResult = result.data as FalOutput; + } catch (err) { + console.error("[qron/generate] fal.ai error:", err); + try { + const admin = adminClient(); + const { data: p } = await admin + .from("profiles") + .select("credits") + .eq("id", userId) + .single(); + if (p) await admin.from("profiles").update({ credits: p.credits + 1 }).eq("id", userId); + } catch { /* best-effort refund */ } + + return NextResponse.json( + { error: "Image generation failed. Credit has been refunded." }, + { status: 500 } + ); + } + + const generatedImageUrl = falResult.image.url; + const seed = falResult.seed; + + const registration = await registerQron({ + userId, + assetUrl: generatedImageUrl, + payloadUrl, + prompt: prompt ?? "", + seed, + }); + + return NextResponse.json({ + imageUrl: generatedImageUrl, + qrDataUrl: generatedImageUrl, + prompt, + url: payloadUrl, + seed, + registration_id: registration.id, + authichain_record_id: registration.authichain_record_id, + qron: { + imageUrl: generatedImageUrl, + destinationUrl: payloadUrl, + prompt, + seed, + registration_id: registration.id, + authichain_record_id: registration.authichain_record_id, + }, + }); +} + +export async function GET() { + return NextResponse.json({ + status: "ok", + backend: "fal-ai/illusion-diffusion", + model: "fal-ai/illusion-diffusion", + version: "direct-v2", + }); +} diff --git a/app/api/qron/register/route.ts b/app/api/qron/register/route.ts new file mode 100644 index 0000000..1d809ba --- /dev/null +++ b/app/api/qron/register/route.ts @@ -0,0 +1,105 @@ +/** + * app/api/qron/register/route.ts + * + * Receives QRON generation data and writes provenance records to Supabase. + * Also forwards to the AuthiChain D1 worker for cross-registration. + */ + +import { NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +export const dynamic = "force-dynamic"; + +function adminClient() { + return createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY! + ); +} + +export async function POST(request: Request) { + const authHeader = request.headers.get("Authorization") ?? ""; + const token = authHeader.replace(/^Bearer\s+/i, "").trim(); + + if (!token || token !== process.env.AUTHICHAIN_API_SECRET) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + let body: Record; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); + } + + if (!body.asset_url || !body.payload_hash) { + return NextResponse.json( + { error: "asset_url and payload_hash are required" }, + { status: 400 } + ); + } + + const admin = adminClient(); + + // Write to Supabase qron_registrations table + const { data, error } = await admin + .from("qron_registrations") + .insert({ + user_id: body.user_id as string | null, + asset_url: body.asset_url as string, + payload_hash: body.payload_hash as string, + payload_preview: (body.payload_preview as string) ?? null, + prompt: (body.prompt as string) ?? null, + seed: (body.seed as number) ?? null, + chain: (body.chain as string) ?? "polygon", + status: (body.status as string) ?? "pending_mint", + source: (body.source as string) ?? "unknown", + registered_at: (body.registered_at as string) ?? new Date().toISOString(), + }) + .select("id") + .single(); + + if (error) { + console.error("[qron/register] Supabase insert error:", error); + return NextResponse.json( + { error: "Database write failed", detail: error.message }, + { status: 500 } + ); + } + + // Forward to AuthiChain D1 worker (non-fatal) + let authichainRecordId: string | null = null; + const workerUrl = process.env.AUTHICHAIN_WORKER_URL; + if (workerUrl) { + try { + const res = await fetch(`${workerUrl}/api/qron-register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.AUTHICHAIN_API_SECRET}`, + }, + body: JSON.stringify({ + qron_id: data.id, + ...body, + }), + signal: AbortSignal.timeout(8_000), + }); + if (res.ok) { + const d = await res.json(); + authichainRecordId = (d as { id?: string }).id ?? null; + } + } catch (err) { + console.warn("[qron/register] D1 cross-reg failed:", err); + } + } + + return NextResponse.json( + { + id: data.id, + authichain_record_id: authichainRecordId, + status: body.status ?? "pending_mint", + chain: body.chain ?? "polygon", + }, + { status: 201 } + ); +} diff --git a/app/api/v1/os/[...slug]/route.ts b/app/api/v1/os/[...slug]/route.ts index 337f6b0..a48fd3c 100644 --- a/app/api/v1/os/[...slug]/route.ts +++ b/app/api/v1/os/[...slug]/route.ts @@ -2,8 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; const SUPA = 'https://nhdnkzhtadfkkluiulhs.supabase.co/functions/v1'; const ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5oZG5remh0YWRma2tsdWl1bGxocyIsInJlbG8iOiJhbm9uIiwiaWF0IjoxNjcyMTkzODIxNSwiZXpAIjoyMDg5NTE0MjE1fQ.akaWgxRilbjavzpsLqU149nBJqxDjbYOnRdAqrwz4J8'; const FN_MAP: Record = { scan: 'authichain-scan', verify: 'authichain-verify', register: 'authichain-register', events: 'authichain-events', story: 'storymode', rewards: 'qron-rewards', apikeys: 'authichain-apikeys' }; -export async function GET(req: NextRequest, { params }: { params: { slug: string[] } }) { return proxy(req, params.slug); } -export async function POST(req: NextRequest, { params }: { params: { slug: string[] } }) { return proxy(req, params.slug); } +export async function GET(req: NextRequest, { params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params; return proxy(req, slug); } +export async function POST(req: NextRequest, { params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params; return proxy(req, slug); } async function proxy(req: NextRequest, slug: string[]) { const action = slug[0]; const fn = FN_MAP[action]; diff --git a/app/login/page.tsx b/app/login/page.tsx index 520d129..37687ab 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -23,9 +23,9 @@ function LoginContent() { const [loading, setLoading] = useState(false) useEffect(() => { - const error = searchParams.get('error') + const error = searchParams?.get('error') if (error) { - const message = searchParams.get('message') || 'Authentication failed. Please try again.' + const message = searchParams?.get('message') || 'Authentication failed. Please try again.' toast({ title: 'Sign-in Error', description: message, variant: 'destructive' }) } }, []) // eslint-disable-line react-hooks/exhaustive-deps diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx index ba1199e..91126e9 100644 --- a/app/pricing/page.tsx +++ b/app/pricing/page.tsx @@ -72,7 +72,7 @@ function PricingContent() { const [checkoutError, setCheckoutError] = useState(null) const router = useRouter() const params = useSearchParams() - const cancelled = params.get('checkout') === 'cancelled' + const cancelled = params?.get('checkout') === 'cancelled' async function handleCheckout(plan: typeof PLANS[0]) { setCheckoutError(null) diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 0000000..e3a4b1c --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,68 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Privacy Policy - AuthiChain", + description: "AuthiChain Privacy Policy — how we collect, use, and protect your data.", +}; + +export default function PrivacyPage() { + return ( +
+
+

Privacy Policy

+

Effective Date: March 29, 2026

+

+ AuthiChain ("we", "our", "us") is committed to protecting your privacy. This + Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit + our website authichain.com, use our services, or interact with us. +

+ +

1. Information We Collect

+
    +
  • Personal Data: Name, email, company (from /me endpoint or sign-up).
  • +
  • Usage Data: IP address, browser type, access times, API calls (/classify, /verify).
  • +
  • Product Data: UPC/GS1 codes, images for verification (not stored long-term).
  • +
+ +

2. How We Use Your Information

+
    +
  • Provide verification services (AI classification, blockchain minting).
  • +
  • Improve services, analytics.
  • +
  • Comply with DSCSA (pharma), METRC (cannabis) standards where applicable.
  • +
  • Send updates (with opt-out).
  • +
+ +

3. Legal Basis (GDPR)

+

Processing is based on contract performance, legitimate interests, or consent.

+ +

4. Sharing Your Information

+
    +
  • Service providers (Supabase, Cloudflare).
  • +
  • Compliance authorities (FDA DSCSA reports if required).
  • +
  • No sale of personal data.
  • +
+ +

5. Data Retention

+

Verification results: 30 days (KV cache); personal data: as needed for service + 1 year.

+ +

6. Your Rights (GDPR/CCPA)

+
    +
  • Access, correct, delete, portability, object.
  • +
  • Contact: privacy@authichain.com
  • +
+ +

7. International Transfers

+

Data processed in US/EU via Supabase/Cloudflare (SCCs in place).

+ +

8. Security

+

HTTPS, API keys, encryption at rest/transit.

+ +

9. Children's Privacy

+

Not for under 13.

+ +

10. Changes

+

Updates posted here.

+
+
+ ); +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx index ae3a033..614a083 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -27,7 +27,7 @@ export default function SignupPage() { const [refCode, setRefCode] = useState("") useEffect(() => { - const ref = searchParams.get("ref") + const ref = searchParams?.get("ref") if (ref) setRefCode(ref.toUpperCase()) }, []) diff --git a/app/sitemap.ts b/app/sitemap.ts index 15e99d5..3e0b93b 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,4 +1,4 @@ -import { MetadataRoute } from "next/dist/lib/metadata/types/metadata-types"; +import type { MetadataRoute } from 'next' export default function sitemap(): MetadataRoute.Sitemap { const base = "https://authichain.com"; const now = new Date(); @@ -25,5 +25,8 @@ export default function sitemap(): MetadataRoute.Sitemap { { url: `${base}/verify`, lastModified: now, changeFrequency: "weekly", priority: 0.7 }, { url: `${base}/pricing`, lastModified: now, changeFrequency: "monthly", priority: 0.65 }, { url: `${base}/eu-dpp`, lastModified: now, changeFrequency: "monthly", priority: 0.65 }, + { url: `${base}/privacy`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 }, + { url: `${base}/terms`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 }, + { url: `${base}/demo-video`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 }, ]; } diff --git a/app/solutions/[industry]/page.tsx b/app/solutions/[industry]/page.tsx index 95c6ab7..08e94a8 100644 --- a/app/solutions/[industry]/page.tsx +++ b/app/solutions/[industry]/page.tsx @@ -248,8 +248,9 @@ export function generateStaticParams() { return Object.keys(industries).map(industry => ({ industry })) } -export function generateMetadata({ params }: { params: { industry: string } }): Metadata { - const data = industries[params.industry] +export async function generateMetadata({ params }: { params: Promise<{ industry: string }> }): Promise { + const { industry } = await params + const data = industries[industry] if (!data) return {} return { title: `${data.name} Authentication | AuthiChain`, @@ -257,14 +258,15 @@ export function generateMetadata({ params }: { params: { industry: string } }): openGraph: { title: `${data.name} Product Authentication | AuthiChain`, description: data.description, - url: `https://authichain.com/solutions/${params.industry}`, + url: `https://authichain.com/solutions/${industry}`, }, - alternates: { canonical: `https://authichain.com/solutions/${params.industry}` }, + alternates: { canonical: `https://authichain.com/solutions/${industry}` }, } } -export default function IndustryPage({ params }: { params: { industry: string } }) { - const data = industries[params.industry] +export default async function IndustryPage({ params }: { params: Promise<{ industry: string }> }) { + const { industry } = await params + const data = industries[industry] if (!data) notFound() return ( @@ -392,7 +394,7 @@ export default function IndustryPage({ params }: { params: { industry: string } "@type": "WebPage", name: `${data.name} Product Authentication | AuthiChain`, description: data.description, - url: `https://authichain.com/solutions/${params.industry}`, + url: `https://authichain.com/solutions/${industry}`, mainEntity: { "@type": "Product", name: `AuthiChain for ${data.name}`, diff --git a/app/terms/page.tsx b/app/terms/page.tsx new file mode 100644 index 0000000..4089868 --- /dev/null +++ b/app/terms/page.tsx @@ -0,0 +1,51 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Terms of Service - AuthiChain", + description: "AuthiChain Terms of Service — usage terms for our product authentication platform.", +}; + +export default function TermsPage() { + return ( +
+
+

Terms of Service

+

Effective Date: March 29, 2026

+ +

1. Acceptance

+

Using AuthiChain (site, API) binds you to these Terms.

+ +

2. Services

+
    +
  • AI product classification (/classify).
  • +
  • Blockchain verification/minting (/verify, /mintNft).
  • +
  • Industry compliance checks.
  • +
+ +

3. Accounts & API Keys

+

Secure your API key; responsible for all activity.

+ +

4. Fees

+

Free tier + paid plans via Stripe/RapidAPI.

+ +

5. Prohibited Use

+
    +
  • No illegal/counterfeit promotion.
  • +
  • No excessive calls (rate limits apply).
  • +
+ +

6. DSCSA/METRC Compliance

+

For pharma/cannabis: Results aid serialization/tracking; not legal advice.

+ +

7. Limitation of Liability

+

No warranties; max liability = fees paid.

+ +

8. Governing Law

+

Michigan, US.

+ +

9. Termination

+

We may suspend for violations.

+
+
+ ); +} diff --git a/app/verify/[truemark_id]/page.tsx b/app/verify/[truemark_id]/page.tsx index 2b1fab2..2467eff 100644 --- a/app/verify/[truemark_id]/page.tsx +++ b/app/verify/[truemark_id]/page.tsx @@ -1,13 +1,15 @@ import type { Metadata } from 'next'; -type Props = { params: { truemark_id: string } }; +type Props = { params: Promise<{ truemark_id: string }> }; export async function generateMetadata({ params }: Props): Promise { - return { title: `AuthiChain - Verify ${params.truemark_id}`, description: 'Blockchain-verified product authentication' }; + const { truemark_id } = await params; + return { title: `AuthiChain - Verify ${truemark_id}`, description: 'Blockchain-verified product authentication' }; } -export default function VerifyPage({ params }: Props) { - const url = `https://nhdnkzhtadfkkluiulhs.supabase.co/functions/v1/authichain-verify-page/${params.truemark_id}`; +export default async function VerifyPage({ params }: Props) { + const { truemark_id } = await params; + const url = `https://nhdnkzhtadfkkluiulhs.supabase.co/functions/v1/authichain-verify-page/${truemark_id}`; return (