Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 4x 12x 4x 1x 1x 1x 3x 3x 3x 1x 2x 2x 1x 1x 1x | import type { NextApiRequest, NextApiResponse } from 'next';
import allowCors from '../../helpers/cors';
import { CACHE, fetchWithTimeout } from '@/helpers/http';
export type DeploymentStatus = 'BUILDING' | 'ERROR' | 'INITIALIZING' | 'QUEUED' | 'READY' | 'CANCELED';
export type DeploymentEnvironment = 'production' | 'preview';
export type DeploymentResponse = {
status: DeploymentStatus;
environment: DeploymentEnvironment;
createdAt: string;
buildingAt: string;
ready: string;
username: string;
};
type DeploymentResponseType = DeploymentResponse | { error: string };
/**
* @description Get the last deployment status
* @param _request NextApiRequest
* @param res NextApiResponse
* @returns {Promise<DeploymentResponse>}
* @example localhost:3000/api/deployments
*/
const REQUIRED_ENV = ['NEXT_PROJECT_ID', 'NEXT_TOKEN', 'NEXT_PUBLIC_ENV'] as const;
const fetchLatestDeployment = async (projectId: string, target: string, token: string) => {
const url = new URL('https://api.vercel.com/v6/deployments');
url.searchParams.set('projectId', projectId);
url.searchParams.set('target', target);
url.searchParams.set('limit', '1');
const result = await fetchWithTimeout(url.toString(), {
headers: { Authorization: `Bearer ${token}` },
method: 'get',
});
const data = await result.json();
return data.deployments?.[0];
};
export default allowCors(async function handler(
_request: NextApiRequest,
res: NextApiResponse<DeploymentResponseType>
) {
const missingEnv = REQUIRED_ENV.filter((name) => !process.env[name]);
if (missingEnv.length > 0) {
console.error(`Missing required environment variables for Vercel API: ${missingEnv.join(', ')}`);
res.setHeader('Cache-Control', CACHE.error);
return res.status(500).json({ error: 'Configuration error' });
}
try {
const deployment = await fetchLatestDeployment(
process.env.NEXT_PROJECT_ID as string,
process.env.NEXT_PUBLIC_ENV as string,
process.env.NEXT_TOKEN as string
);
if (!deployment) {
throw new Error('No deployment found');
}
res.setHeader('Cache-Control', CACHE.deployment);
return res.status(200).json({
status: deployment.state,
environment: process.env.NEXT_PUBLIC_ENV as DeploymentEnvironment,
createdAt: deployment.createdAt,
buildingAt: deployment.buildingAt,
ready: deployment.ready,
// Kept deliberately. The audit flagged it as reconnaissance, but DeploymentStatus renders
// it in the widget tooltip, and the value is the owner's own account name — already public
// as the org in this repository's URL. Removing it would break a feature to hide
// something that is not hidden.
username: deployment.creator?.username,
});
} catch (err: unknown) {
// Was `err.message`, the only route of eight that echoed internal error text to the client.
// A non-JSON Vercel error page surfaces a SyntaxError carrying a fragment of the upstream
// body, and a fetch failure surfaces Node's internal text — an uncontrolled channel from
// server internals to an anonymous caller.
console.error('Deployments API Error:', err);
res.setHeader('Cache-Control', CACHE.error);
return res.status(500).json({ error: 'Internal server error' });
}
});
|