Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 <your-domain>/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)
Expand Down
1 change: 0 additions & 1 deletion .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 3 additions & 36 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,54 +25,21 @@ 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
with:
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 }}
accountId: ${{ secrets.BURNT_CLOUDFLARE_ACCOUNT_ID }}
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,
});
86 changes: 3 additions & 83 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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,
});
}
39 changes: 9 additions & 30 deletions src/app/api/oauth3/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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 = `<!DOCTYPE html>
<html><head><title>Authenticated</title></head>
<body>
Expand All @@ -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";
}
</script>
<p>Authenticated. You can close this window.</p>
</body></html>`;

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;
}
23 changes: 8 additions & 15 deletions src/app/api/oauth3/login/route.ts
Original file line number Diff line number Diff line change
@@ -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);
}
26 changes: 9 additions & 17 deletions src/app/api/oauth3/verify/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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" }),
});
Expand All @@ -67,17 +69,7 @@ export async function POST(request: NextRequest) {
}

const data = await res.json(); // { result: "<json string>", quote: "<base64>" }
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(
Expand Down
6 changes: 3 additions & 3 deletions src/app/badge/[address]/BadgeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ export default function BadgeContent({ address }: { address: string }) {
<button
onClick={async () => {
try {
await navigator.clipboard.writeText("https://theredactedfile.com");
await navigator.clipboard.writeText(window.location.origin);
} catch {
const input = document.createElement("input");
input.value = "https://theredactedfile.com";
input.value = window.location.origin;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
Expand All @@ -218,7 +218,7 @@ export default function BadgeContent({ address }: { address: string }) {
{copied ? "Copied!" : "Copy Link"}
</button>
<a
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent("I wasn\u2019t on the Epstein list. Were you?\n\nProve it.\n\nhttps://theredactedfile.com")}`}
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent("I wasn\u2019t on the Epstein list. Were you?\n\nProve it.\n\n")}${encodeURIComponent(window.location.origin)}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[11px] tracking-[0.1em] uppercase text-fg-muted border border-border px-4 py-2 no-underline transition-all hover:border-fg hover:text-fg text-center"
Expand Down
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function RootLayout({
authentication: {
type: "redirect" as const,
callbackUrl: typeof window !== "undefined"
? window.location.origin + window.location.pathname
? window.location.origin + "/verify"
: undefined,
},
}), []);
Expand Down
4 changes: 2 additions & 2 deletions src/app/overloaded/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ export default function OverloadedPage() {

const handleCopyLink = useCallback(async () => {
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");
Expand Down