Skip to content
Open
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
68 changes: 22 additions & 46 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
# Dependencies
# ── Dependencies ──────────────────────────────────────────────────────────
node_modules/
package-lock.json

# Next.js build output
# ── Next.js ───────────────────────────────────────────────────────────────
.next/
out/

# Environment variables
# Environment files
# ── Build outputs ─────────────────────────────────────────────────────────
dist/

# ── Environment variables ─────────────────────────────────────────────────
.env
.env.local
.env.*.local

# TypeScript build info
*.tsbuildinfo
# TypeScript
# ── TypeScript ────────────────────────────────────────────────────────────
*.tsbuildinfo
next-env.d.ts

# OS files
.DS_Store
Thumbs.db

# Test artifacts
# ── Test artifacts ────────────────────────────────────────────────────────
coverage/
__snapshots__/
**/__snapshots__/
*.snap
playwright-report/
test-results/
junit.xml
*.lcov

# Editor / IDE
# ── OS files ──────────────────────────────────────────────────────────────
.DS_Store
Thumbs.db

# ── Editor / IDE ──────────────────────────────────────────────────────────
.vscode/
.idea/
*.swp
Expand All @@ -39,42 +41,16 @@ __snapshots__/
*.sln
*.sw?

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
*.log

# Misc
.vercel
.eslintcache
*.DS_Store

# Test artifacts
**/__snapshots__/
*.snap
coverage/
playwright-report/
test-results/
junit.xml
*.lcov

# Logs
# ── Logs ──────────────────────────────────────────────────────────────────
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor
.vscode/
.idea/
*.swp
*.swo

# Misc
.turbo/
# ── Misc ──────────────────────────────────────────────────────────────────
.vercel/
.turbo/
.cache/
.eslintcache
*.tgz
dist/
64 changes: 35 additions & 29 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
const withBundleAnalyzer = require("@Next/bundle-analyzer")(true);
const { withSentryConfig } = require("@sentry/nextjs");
const withPWA = require("next-pwa")(true);
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
const { withSentryConfig } = require('@sentry/nextjs');
const withPWA = require('next-pwa')({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
});

const nextConfig = {
experimental: {
serverComponentsExternalPackages: ["@stellar/stellar-sdk", "@vercel/blob"],
serverComponentsExternalPackages: ['@stellar/stellar-sdk', '@vercel/blob'],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "www.gravatar.com",
pathname: "/avatar/**",
protocol: 'https',
hostname: 'www.gravatar.com',
pathname: '/avatar/**',
},
],
},
async headers() {
return [
{
source: "/:path*",
source: '/:path*',
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=(), payment=(self)",
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=(self)',
},
],
},
{
source: "/sw.js",
source: '/sw.js',
headers: [
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
{ key: "Service-Worker-Allowed", value: "/" },
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
{ key: 'Service-Worker-Allowed', value: '/' },
],
},
{
source: "/embed/:id",
source: '/embed/:id',
headers: [
{ key: "X-Frame-Options", value: "ALLOWALL" },
{ key: 'X-Frame-Options', value: 'ALLOWALL' },
],
},
];
Expand All @@ -59,16 +64,17 @@ const nextConfig = {
}
config.externals = [
...(Array.isArray(config.externals) ? config.externals : []),
"sodium-native",
...(isServer ? [{
"@stellar/stellar-sdk": "commonjs2 @stellar/stellar-sdk",
}, {
"@vercel/blob": "commonjs2 @vercel/blob",
}] : []),
'sodium-native',
...(isServer
? [
{ '@stellar/stellar-sdk': 'commonjs2 @stellar/stellar-sdk' },
{ '@vercel/blob': 'commonjs2 @vercel/blob' },
]
: []),
];
config.resolve.alias = {
...config.resolve.alias,
"@apm-js-collab/tracing-hooks": false,
'@apm-js-collab/tracing-hooks': false,
};
return config;
},
Expand All @@ -77,7 +83,7 @@ const nextConfig = {
const sentryWebpackPluginOptions = {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTHToKEN,
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: true,
disableServerWebpackPlugin: !process.env.NEXT_PUBLIC_SENTRY_DSN,
disableClientWebpackPlugin: !process.env.NEXT_PUBLIC_SENTRY_DSN,
Expand All @@ -86,4 +92,4 @@ const sentryWebpackPluginOptions = {
module.exports = withSentryConfig(
withBundleAnalyzer(withPWA(nextConfig)),
sentryWebpackPluginOptions
|);
);
94 changes: 94 additions & 0 deletions src/app/api/auth/challenge/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import {
TransactionBuilder,
Networks,
BASE_FEE,
Operation,
Memo,
Account,
Keypair,
} from '@stellar/stellar-sdk';

const HOME_DOMAIN = process.env.SEP10_HOME_DOMAIN ?? 'stellarsplit.app';
const SERVER_SIGNING_KEY = process.env.SEP10_SERVER_SIGNING_KEY ?? '';

/**
* GET /api/auth/challenge?account={publicKey}
*
* Returns a base64-encoded unsigned SEP-0010 challenge transaction
* valid for 5 minutes. The client must sign it with Freighter and
* POST the result to /api/auth/token.
*/
export async function GET(req: NextRequest) {
const account = req.nextUrl.searchParams.get('account');
if (!account) {
return NextResponse.json({ error: 'Missing account parameter' }, { status: 400 });
}

// Basic Stellar public key format check
if (!/^G[A-Z2-7]{55}$/.test(account)) {
return NextResponse.json({ error: 'Invalid account format' }, { status: 400 });
}

if (!SERVER_SIGNING_KEY) {
return NextResponse.json(
{ error: 'Server signing key not configured' },
{ status: 500 }
);
}

let serverKeypair: ReturnType<typeof Keypair.fromSecret>;
try {
serverKeypair = Keypair.fromSecret(SERVER_SIGNING_KEY);
} catch {
return NextResponse.json(
{ error: 'Invalid server signing key configuration' },
{ status: 500 }
);
}

const now = Math.floor(Date.now() / 1000);
const expiry = now + 300; // 5 minutes
const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(48))).toString('base64');

// SEP-0010 spec: use the client account as the transaction source,
// with a sequence number of 0 (challenge tx is never submitted on-chain).
const sourceAccount = new Account(account, '-1');

const tx = new TransactionBuilder(sourceAccount, {
fee: BASE_FEE,
networkPassphrase:
process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'
? Networks.PUBLIC
: Networks.TESTNET,
timebounds: { minTime: now, maxTime: expiry },
})
.addOperation(
Operation.manageData({
name: `${HOME_DOMAIN} auth`,
value: nonce,
source: serverKeypair.publicKey(),
})
)
.build();

// The server signs its own ManageData operation source
tx.sign(serverKeypair);

const xdr = tx.toEnvelope().toXDR('base64');

return NextResponse.json(
{
transaction: xdr,
network_passphrase:
process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet'
? Networks.PUBLIC
: Networks.TESTNET,
},
{
headers: {
'Cache-Control': 'no-store',
},
}
);
}
Loading