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 src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000';
import { resolveEnv } from './env';

const API_URL = resolveEnv('NEXT_PUBLIC_API_URL', process.env.NEXT_PUBLIC_API_URL, 'http://localhost:3000');
const TOKEN_KEY = 'stellartickets.token';

export class ApiError extends Error {
Expand Down
41 changes: 41 additions & 0 deletions src/lib/env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { resolveEnv } from './env';

const originalNodeEnv = process.env.NODE_ENV;

afterEach(() => {
vi.stubEnv('NODE_ENV', originalNodeEnv ?? 'test');
});

describe('resolveEnv', () => {
it('returns the value when it is set, regardless of NODE_ENV', () => {
vi.stubEnv('NODE_ENV', 'production');
expect(resolveEnv('X', 'https://api.example.com', 'http://localhost:3000')).toBe(
'https://api.example.com',
);
});

it('falls back to the dev default outside production', () => {
vi.stubEnv('NODE_ENV', 'development');
expect(resolveEnv('X', undefined, 'http://localhost:3000')).toBe('http://localhost:3000');
});

it('falls back to the dev default in test', () => {
vi.stubEnv('NODE_ENV', 'test');
expect(resolveEnv('X', undefined, 'http://localhost:3000')).toBe('http://localhost:3000');
});

it('throws instead of silently falling back when missing in production', () => {
vi.stubEnv('NODE_ENV', 'production');
expect(() => resolveEnv('NEXT_PUBLIC_API_URL', undefined, 'http://localhost:3000')).toThrow(
/NEXT_PUBLIC_API_URL/,
);
});

it('treats an empty string the same as missing', () => {
vi.stubEnv('NODE_ENV', 'production');
expect(() => resolveEnv('NEXT_PUBLIC_API_URL', '', 'http://localhost:3000')).toThrow(
/NEXT_PUBLIC_API_URL/,
);
});
});
21 changes: 21 additions & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Guards against a public env var silently falling back to a dev-only
* default in a production build (#37). `api.ts`'s `NEXT_PUBLIC_API_URL`
* fallback (`http://localhost:3000`) is the case that prompted this: if the
* var is forgotten when building for production, every visitor's browser
* tries to call *its own* localhost instead of the real backend, and the
* failure looks like a backend outage rather than a config mistake.
*
* `NODE_ENV === 'production'` is Next.js's own signal for "this is a real
* build", set automatically by `next build` — not something this app sets
* itself — so this doesn't need a separate build-time flag.
*/
export function resolveEnv(name: string, value: string | undefined, devFallback: string): string {
if (value) return value;
if (process.env.NODE_ENV === 'production') {
throw new Error(
`Missing required environment variable: ${name}. This must be set for production builds.`,
);
}
return devFallback;
}
8 changes: 6 additions & 2 deletions src/lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import {
getAddress,
signTransaction as freighterSignTransaction,
} from '@stellar/freighter-api';
import { resolveEnv } from './env';

const NETWORK_PASSPHRASE =
process.env.NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE ?? 'Test SDF Network ; September 2015';
const NETWORK_PASSPHRASE = resolveEnv(
'NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE',
process.env.NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE,
'Test SDF Network ; September 2015',
);

export class WalletError extends Error {}

Expand Down