Compare commits

...

2 commits

Author SHA1 Message Date
AeThex
a57cdb029a chore: remove vercel.json — site now served from VPS Docker container
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:54:54 +00:00
AeThex
f1bcc957f9 fix: Discord Activity token exchange, CSP headers, subscription routes, and static asset 404
- Remove redirect_uri from Discord token exchange (Activities use proxy auth, not redirect flow)
- Add Content-Security-Policy with frame-ancestors for Discord embedding (was only in vercel.json)
- Wire up subscription create-checkout and manage routes in Express
- Add Studio arm to ArmSwitcher with external link
- Prevent SPA catch-all from serving HTML for missing static assets (fixes script.js Unexpected token error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:49:50 +00:00
5 changed files with 37 additions and 166 deletions

View file

@ -38,9 +38,6 @@ export default async function handler(req: any, res: any) {
client_secret: clientSecret,
grant_type: "authorization_code",
code,
redirect_uri:
process.env.DISCORD_ACTIVITY_REDIRECT_URI ||
"https://aethex.dev/activity",
}).toString(),
},
);

View file

@ -68,9 +68,22 @@ const ARMS: Arm[] = [
textColor: "text-purple-400",
href: "/staff",
},
{
id: "studio",
name: "AeThex | Studio",
label: "Studio",
color: "#00ffff",
bgColor: "bg-cyan-500/20",
textColor: "text-cyan-400",
href: "https://aethex.studio",
external: true,
},
];
const STUDIO_SVG = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="96" fill="%23050505"/><polygon points="256,48 444,152 444,360 256,464 68,360 68,152" fill="none" stroke="%2300ffff" stroke-width="18" opacity="0.9"/><text x="256" y="320" text-anchor="middle" font-family="Orbitron,monospace" font-size="220" font-weight="700" fill="%2300ffff">&#198;</text></svg>')}`;
const LOGO_URLS: Record<string, string> = {
studio: STUDIO_SVG,
staff:
"https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fc0414efd7af54ef4b821a05d469150d0?format=webp&width=800",
labs: "https://cdn.builder.io/api/v1/image/assets%2Ffc53d607e21d497595ac97e0637001a1%2Fd93f7113d34347469e74421c3a3412e5?format=webp&width=800",

View file

@ -41,6 +41,8 @@ import blogIndexHandler from "../api/blog/index";
import blogSlugHandler from "../api/blog/[slug]";
import aiChatHandler from "../api/ai/chat";
import aiTitleHandler from "../api/ai/title";
import createCheckoutHandler from "../api/subscriptions/create-checkout";
import manageSubscriptionHandler from "../api/subscriptions/manage";
// Developer API Keys handlers
import {
@ -340,6 +342,11 @@ export function createServer() {
app.use((req, res, next) => {
// Allow embedding in iframes (Discord Activities need this)
res.setHeader("X-Frame-Options", "ALLOWALL");
// CSP with frame-ancestors for Discord Activity embedding
res.setHeader(
"Content-Security-Policy",
"default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-ancestors 'self' https://discord.com https://*.discord.com https://*.discordsays.com",
);
// Allow Discord to access the iframe
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
@ -1815,9 +1822,6 @@ export function createServer() {
client_secret: clientSecret,
grant_type: "authorization_code",
code,
redirect_uri:
process.env.DISCORD_ACTIVITY_REDIRECT_URI ||
"https://aethex.dev/activity",
}).toString(),
},
);
@ -8177,5 +8181,16 @@ export function createServer() {
app.post("/api/ai/chat", aiChatHandler);
app.post("/api/ai/title", aiTitleHandler);
// Subscription API routes
app.post("/api/subscriptions/create-checkout", (req: express.Request, res: express.Response) => {
return createCheckoutHandler(req as any, res as any);
});
app.get("/api/subscriptions/manage", (req: express.Request, res: express.Response) => {
return manageSubscriptionHandler(req as any, res as any);
});
app.post("/api/subscriptions/manage", (req: express.Request, res: express.Response) => {
return manageSubscriptionHandler(req as any, res as any);
});
return app;
}

View file

@ -335,6 +335,12 @@ app.get("*", async (req, res) => {
return res.status(404).json({ error: "API endpoint not found" });
}
// Don't serve the SPA shell for missing static-asset requests — they should 404
// cleanly rather than returning HTML (which causes "Unexpected token '<'" JS errors).
if (/\.(js|mjs|cjs|css|map|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|webp|avif)$/i.test(req.path)) {
return res.status(404).send("Not found");
}
try {
const template = getTemplate();
const meta = await resolveRouteMeta(req.path);

View file

@ -1,160 +0,0 @@
{
"version": 2,
"buildCommand": "npm run build",
"outputDirectory": "dist/spa",
"functions": {
"api/**/*.ts": {
"memory": 1024,
"maxDuration": 30
}
},
"redirects": [
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.app" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.locker" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.studio" }],
"destination": "https://aethex.dev/ethos/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.info" }],
"destination": "https://aethex.dev/foundation/:path",
"permanent": true
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "aethex.site" }],
"destination": "https://aethex.dev/:path",
"permanent": true
},
{
"source": "/",
"has": [{ "type": "host", "value": "aethex.me" }],
"destination": "https://aethex.dev/",
"permanent": true
},
{
"source": "/",
"has": [{ "type": "host", "value": "aethex.space" }],
"destination": "https://aethex.dev/",
"permanent": true
},
{
"source": "/feed",
"destination": "/community/feed",
"permanent": true
}
],
"rewrites": [
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<proxy>.+)\\.discordsays\\.com" }],
"destination": "/index.html"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<sub>.+)\\.aethex\\.me" }],
"destination": "/index.html"
},
{
"source": "/:path(.*)",
"has": [{ "type": "host", "value": "(?<sub>.+)\\.aethex\\.space" }],
"destination": "/index.html"
},
{
"source": "/api/:path(.*)",
"destination": "/api/:path"
},
{ "source": "/", "destination": "/index.html" },
{ "source": "/login", "destination": "/index.html" },
{ "source": "/login/:path*", "destination": "/index.html" },
{ "source": "/dashboard", "destination": "/index.html" },
{ "source": "/dashboard/:path*", "destination": "/index.html" },
{ "source": "/profile", "destination": "/index.html" },
{ "source": "/profile/:path*", "destination": "/index.html" },
{ "source": "/activity", "destination": "/index.html" },
{ "source": "/activity/", "destination": "/index.html" },
{ "source": "/activity/:path*", "destination": "/index.html" },
{ "source": "/admin", "destination": "/index.html" },
{ "source": "/admin/:path*", "destination": "/index.html" },
{ "source": "/creators", "destination": "/index.html" },
{ "source": "/creators/:path*", "destination": "/index.html" },
{ "source": "/opportunities", "destination": "/index.html" },
{ "source": "/opportunities/:path*", "destination": "/index.html" },
{ "source": "/nexus", "destination": "/index.html" },
{ "source": "/nexus/:path*", "destination": "/index.html" },
{ "source": "/foundation", "destination": "/index.html" },
{ "source": "/foundation/:path*", "destination": "/index.html" },
{ "source": "/gameforge", "destination": "/index.html" },
{ "source": "/gameforge/:path*", "destination": "/index.html" },
{ "source": "/labs", "destination": "/index.html" },
{ "source": "/labs/:path*", "destination": "/index.html" },
{ "source": "/corp", "destination": "/index.html" },
{ "source": "/corp/:path*", "destination": "/index.html" },
{ "source": "/devlink", "destination": "/index.html" },
{ "source": "/devlink/:path*", "destination": "/index.html" },
{ "source": "/community", "destination": "/index.html" },
{ "source": "/community/:path*", "destination": "/index.html" },
{ "source": "/developers", "destination": "/index.html" },
{ "source": "/developers/:path*", "destination": "/index.html" },
{ "source": "/discord-verify", "destination": "/index.html" },
{ "source": "/discord-verify/:path*", "destination": "/index.html" },
{ "source": "/ethos", "destination": "/index.html" },
{ "source": "/ethos/:path*", "destination": "/index.html" },
{ "source": "/:path*", "destination": "/index.html" }
],
"headers": [
{
"source": "/assets/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/(.*).(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff2)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/api/(.*)",
"headers": [{ "key": "Cache-Control", "value": "no-store" }]
},
{
"source": "/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "geolocation=(), microphone=(), camera=()"
},
{
"key": "Content-Security-Policy",
"value": "default-src 'self' https: data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; frame-ancestors 'self' https://discord.com https://*.discord.com https://*.discordsays.com"
}
]
}
]
}