diff --git a/.env.example b/.env.example index 5183246..9411e28 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,10 @@ NEXT_PUBLIC_NFT_CONTRACT=xion1... NEXT_PUBLIC_ENABLE_OAUTH3=false # OAuth3 (TEE-attested OAuth proxy) +# Server-only: the CVM origin that the /oauth3 reverse proxy forwards to. +# The CVM's APP_PUBLIC_URL must be set to /oauth3 so Google's +# redirect_uri goes back through the proxy (e.g. https://theredactedfile.com/oauth3). OAUTH3_BASE_URL=https://your-oauth3-instance.phala.cloud -NEXT_PUBLIC_OAUTH3_BASE_URL=https://your-oauth3-instance.phala.cloud # Reclaim Protocol (zkTLS verification) # Server-only secrets (never exposed to client) diff --git a/.env.production b/.env.production index 08b373c..e246a6f 100644 --- a/.env.production +++ b/.env.production @@ -20,7 +20,6 @@ NEXT_PUBLIC_RECLAIM_CLEARANCE_CONTRACT=xion1naum74xam7ff684n6yewvnc0k50hqg7q9zkg # OAuth3 (TEE-attested OAuth proxy) NEXT_PUBLIC_ENABLE_OAUTH3=true NEXT_PUBLIC_GOOGLE_BLOCKED=false -NEXT_PUBLIC_OAUTH3_BASE_URL=https://oauth3.burnt.com # Droplinked Store NEXT_PUBLIC_DROPLINKED_SHOP_ID=69a083eab7618f1bcaeaf330 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5e3ceb8..1396277 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,7 +25,9 @@ jobs: deploy: needs: build runs-on: ubuntu-latest - environment: production + environment: + name: production + url: https://redacted.burnt.workers.dev steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -33,24 +35,7 @@ jobs: node-version: lts/* - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - - name: Create GitHub deployment - id: deployment - uses: actions/github-script@v7 - with: - script: | - const deployment = await github.rest.repos.createDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.sha, - environment: 'production', - auto_merge: false, - required_contexts: [], - production_environment: true, - description: 'Production deploy', - }); - core.setOutput('deployment_id', deployment.data.id); - name: Deploy to Cloudflare Workers - id: wrangler uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.BURNT_CLOUDFLARE_API_TOKEN }} @@ -58,21 +43,3 @@ jobs: command: deploy --keep-vars packageManager: pnpm preCommands: pnpm build:worker - - name: Update deployment status - if: always() - uses: actions/github-script@v7 - with: - script: | - const deploymentId = ${{ steps.deployment.outputs.deployment_id }}; - const success = '${{ steps.wrangler.outcome }}' === 'success'; - - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deploymentId, - state: success ? 'success' : 'failure', - environment_url: success ? 'https://redacted.burnt.workers.dev' : undefined, - description: success ? 'Deployed to production' : 'Deploy failed', - environment: 'production', - auto_inactive: true, - }); diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 0d43fa0..42c405d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -7,11 +7,11 @@ on: jobs: preview: runs-on: ubuntu-latest - environment: preview + environment: + name: preview + url: ${{ steps.extract.outputs.preview_url }} permissions: contents: read - pull-requests: write - deployments: write # All env vars are read from .env.production (committed to repo). # RECLAIM_APP_SECRET is a Cloudflare Workers runtime secret (wrangler secret put). env: @@ -25,23 +25,6 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm lint - run: pnpm build - - name: Create GitHub deployment - id: deployment - uses: actions/github-script@v7 - with: - script: | - const deployment = await github.rest.repos.createDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: context.payload.pull_request.head.sha, - environment: 'preview', - auto_merge: false, - required_contexts: [], - transient_environment: true, - production_environment: false, - description: `PR #${context.issue.number} preview`, - }); - core.setOutput('deployment_id', deployment.data.id); - name: Upload preview version id: preview uses: cloudflare/wrangler-action@v3 @@ -59,66 +42,3 @@ jobs: PREVIEW_URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.workers\.dev[^\s]*' | head -1 || echo '') echo "version_id=$VERSION_ID" >> "$GITHUB_OUTPUT" echo "preview_url=$PREVIEW_URL" >> "$GITHUB_OUTPUT" - - name: Update deployment status - if: always() - uses: actions/github-script@v7 - with: - script: | - const deploymentId = ${{ steps.deployment.outputs.deployment_id }}; - const versionId = '${{ steps.extract.outputs.version_id }}'; - const previewUrl = '${{ steps.extract.outputs.preview_url }}'; - const success = '${{ steps.preview.outcome }}' === 'success'; - - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deploymentId, - state: success ? 'success' : 'failure', - environment_url: previewUrl || undefined, - description: success ? `Version ${versionId} uploaded` : 'Preview build failed', - environment: 'preview', - auto_inactive: true, - }); - - name: Comment preview info - if: success() - uses: actions/github-script@v7 - with: - script: | - const versionId = '${{ steps.extract.outputs.version_id }}'; - const previewUrl = '${{ steps.extract.outputs.preview_url }}'; - - const body = [ - '### 🚀 Preview Deployment', - '', - `**Version ID:** \`${versionId}\``, - previewUrl ? `**Preview URL:** ${previewUrl}` : '', - '', - `To deploy: \`npx wrangler versions deploy ${versionId} --percentage 100\``, - ].filter(Boolean).join('\n'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existing = comments.find(c => - c.user.login === 'github-actions[bot]' && - c.body.includes('### 🚀 Preview Deployment') - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/src/app/api/oauth3/callback/route.ts b/src/app/api/oauth3/callback/route.ts index ea90f66..e4149aa 100644 --- a/src/app/api/oauth3/callback/route.ts +++ b/src/app/api/oauth3/callback/route.ts @@ -1,23 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; -// Alphanumeric + common token chars (JWT dots, base64 slashes, etc.), max 2048 -const TOKEN_PATTERN = /^[a-zA-Z0-9._\-~+/=]{1,2048}$/; +// After Google auth the CVM redirects here. +// The CVM's `sid` session cookie is already set on this domain +// (via the /oauth3 reverse proxy), so no token handling is needed. +// +// Desktop: this page runs inside a popup — notify the parent via +// postMessage and close. +// Mobile: no popup, so redirect to /verify with a UI signal. export async function GET(request: NextRequest) { - const token = request.nextUrl.searchParams.get("token") ?? ""; - - // Reject malformed tokens before storing as cookie - if (token && !TOKEN_PATTERN.test(token)) { - return NextResponse.json( - { error: "Invalid token format" }, - { status: 400 } - ); - } - - // This page runs inside a popup window opened by VerifyFlow. - // After OAuth3 login, we store the session token in a cookie on localhost - // and notify the parent window via postMessage. const origin = request.nextUrl.origin; + const html = ` Authenticated @@ -26,30 +19,16 @@ export async function GET(request: NextRequest) { window.opener.postMessage({ type: "oauth3:authenticated" }, ${JSON.stringify(origin)}); window.close(); } else { - // Fallback: if opened directly (not as popup), redirect to verify page window.location.href = "/verify?oauth3=authenticated"; }

Authenticated. You can close this window.

`; - const response = new NextResponse(html, { + return new NextResponse(html, { headers: { "Content-Type": "text/html", "Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'", }, }); - - // Store the OAuth3 session token in an httpOnly cookie on localhost - if (token) { - response.cookies.set("oauth3_token", token, { - path: "/", - httpOnly: true, - sameSite: "lax", - secure: process.env.NODE_ENV === "production", - maxAge: 600, // 10 minutes (matches token expiry) - }); - } - - return response; } diff --git a/src/app/api/oauth3/login/route.ts b/src/app/api/oauth3/login/route.ts index 44dab25..102824b 100644 --- a/src/app/api/oauth3/login/route.ts +++ b/src/app/api/oauth3/login/route.ts @@ -1,21 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; -// The OAuth flow goes through the /oauth3 proxy route so that the CVM's -// session cookie is set on the same origin Google redirects back to. -// The proxy uses `redirect: "manual"` to avoid serving Google's HTML -// under our domain (which would break CSP / CORS). -const PROD_URL = "https://theredactedfile.com"; +// Redirect through the /oauth3 reverse-proxy to start Google auth. +// The proxy forwards requests to the CVM with `redirect: "manual"`, +// so the CVM's 302 → Google is forwarded as-is to the browser. +// +// Cookies (CSRF state, session) are set on theredactedfile.com by +// the proxy — no tokens ever appear in URLs. export async function GET(request: NextRequest) { - const host = request.nextUrl.hostname; - - // For local dev and Cloudflare Workers preview deployments, use the actual - // request origin so the callback and proxy route resolve to the right host. - // In production, use PROD_URL for the canonical domain. - const isPreview = host === "localhost" || host.endsWith(".workers.dev"); - const origin = isPreview ? request.nextUrl.origin : PROD_URL; - - const returnTo = encodeURIComponent(`${origin}/api/oauth3/callback`); - const authUrl = `${origin}/oauth3/auth/google?return_to=${returnTo}`; + const returnTo = encodeURIComponent("/api/oauth3/callback"); + const authUrl = `${request.nextUrl.origin}/oauth3/auth/google?return_to=${returnTo}`; return NextResponse.redirect(authUrl); } diff --git a/src/app/api/oauth3/verify/route.ts b/src/app/api/oauth3/verify/route.ts index e3039b5..4308f59 100644 --- a/src/app/api/oauth3/verify/route.ts +++ b/src/app/api/oauth3/verify/route.ts @@ -10,9 +10,11 @@ export async function POST(request: NextRequest) { ); } - // Read the OAuth3 session token from our localhost cookie - const token = request.cookies.get("oauth3_token")?.value; - if (!token) { + // The CVM's session cookie (`sid`) was set on this domain by the + // /oauth3 reverse proxy during Google auth. Forward it to the CVM + // so the SessionUser extractor can authenticate the request. + const sid = request.cookies.get("sid")?.value; + if (!sid) { return NextResponse.json( { error: "Not authenticated with OAuth3. Please sign in first." }, { status: 401 } @@ -46,13 +48,13 @@ export async function POST(request: NextRequest) { try { // Single call to OAuth3's TEE-attested verification endpoint. - // The CVM should revoke the Google OAuth token immediately after - // completing the Gmail search (POST https://oauth2.googleapis.com/revoke). + // The CVM revokes the Google OAuth token immediately after + // completing the Gmail search and unlinks the identity. const res = await fetch(`${OAUTH3_BASE_URL}/verify/gmail`, { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Cookie: `sid=${sid}`, }, body: JSON.stringify({ address, suspect: "jeevacation@gmail.com" }), }); @@ -67,17 +69,7 @@ export async function POST(request: NextRequest) { } const data = await res.json(); // { result: "", quote: "" } - const response = NextResponse.json(data); - - // Delete the OAuth3 token — match all attributes from when it was set - response.cookies.set("oauth3_token", "", { - path: "/", - httpOnly: true, - sameSite: "lax", - secure: process.env.NODE_ENV === "production", - maxAge: 0, - }); - return response; + return NextResponse.json(data); } catch (e) { console.error("OAuth3 verify error:", e instanceof Error ? e.message : "Unknown error"); return NextResponse.json( diff --git a/src/app/badge/[address]/BadgeContent.tsx b/src/app/badge/[address]/BadgeContent.tsx index 4dd8645..5e19908 100644 --- a/src/app/badge/[address]/BadgeContent.tsx +++ b/src/app/badge/[address]/BadgeContent.tsx @@ -201,10 +201,10 @@ export default function BadgeContent({ address }: { address: string }) { { try { - await navigator.clipboard.writeText("https://theredactedfile.com"); + await navigator.clipboard.writeText(window.location.origin); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Fallback for older browsers const input = document.createElement("input"); - input.value = "https://theredactedfile.com"; + input.value = window.location.origin; document.body.appendChild(input); input.select(); document.execCommand("copy");