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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 1x 1x 1x 1x 1x 9x 1x 1x 6x 6x 6x 6x 6x 6x 1x 5x 5x 5x 7x 21x 7x 1x 1x 1x 6x 6x 5x 1x 4x 4x 1x 3x 3x 3x 3x 3x | import type { NextApiRequest, NextApiResponse } from 'next';
import allowCors from '../../helpers/cors';
import { CACHE, fetchWithTimeout } from '@/helpers/http';
import { deploymentStatusSchema, describeIssues } from '../../types/schemas';
import { vercelDeploymentsSchema } from '../../types/upstream';
import type { DeploymentData, DeploymentEnvironment } from '../../types/api';
/**
* SDD-L07: `DeploymentStatus`, `DeploymentEnvironment` and this response shape were declared here
* AND in `types/api.ts`, with the same names and no import between them. Two declarations of one
* contract drift the moment either side is edited, and this pair had already drifted from the
* upstream API they describe: the status union listed six states where Vercel documents eight, and
* `DeploymentStatus` renders `styles[status.toLowerCase()]`, so `BLOCKED` or `DELETED` produced an
* unstyled dot with no fallback.
*
* The local copies are gone. `types/api.ts` re-exports the inferred types, so the old import path
* still resolves for anything outside this file.
*/
export type { DeploymentStatus, DeploymentEnvironment, DeploymentData as DeploymentResponse } from '../../types/api';
type DeploymentResponseType = DeploymentData | { error: string };
/**
* @description Convert a Vercel unix-millisecond timestamp into the ISO string the contract declares.
*
* The route used to forward these three fields untouched under a `string` annotation while Vercel
* documents all of them as `number`. `new Date()` accepts either, so `DeploymentStatus`'s tooltip
* kept working and the mismatch stayed invisible — a type that is wrong but never observed. Sending
* an ISO string makes the declaration true rather than lucky, and it is what a tooltip wants anyway.
*/
const toIsoString = (timestamp: number | undefined): string =>
typeof timestamp === 'number' ? new Date(timestamp).toISOString() : '';
/**
* @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',
});
// Was missing entirely: on a 401 from a rotated token, or a 429, Vercel answers a JSON error
// envelope with no `deployments` key. `data.deployments?.[0]` then quietly produced `undefined`
// and the handler reported "No deployment found" — the same message as a project that has never
// deployed. Two very different situations, one indistinguishable log line.
if (!result.ok) {
throw new Error(`Vercel API responded ${result.status}`);
}
const parsed = vercelDeploymentsSchema.safeParse(await result.json());
Iif (!parsed.success) {
throw new Error(`Vercel API response did not match: ${describeIssues(parsed.error.issues)}`);
}
return parsed.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');
}
// `state` is optional upstream and `readyState` is the required one, so fall back rather
// than trust it. Validating against the union here is what keeps the widget's
// `styles[status.toLowerCase()]` lookup from silently missing: a state Vercel adds later
// fails loudly at this line instead of rendering an invisible dot.
const state = deploymentStatusSchema.safeParse(deployment.state ?? deployment.readyState);
if (!state.success) {
throw new Error(`Unknown Vercel deployment state: ${String(deployment.state ?? deployment.readyState)}`);
}
res.setHeader('Cache-Control', CACHE.deployment);
return res.status(200).json({
status: state.data,
environment: process.env.NEXT_PUBLIC_ENV as DeploymentEnvironment,
createdAt: toIsoString(deployment.createdAt),
buildingAt: toIsoString(deployment.buildingAt),
ready: toIsoString(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.
// `creator.username` is optional upstream — only `creator.uid` is required — so the
// contract's `username: string` needed a value for the case where Vercel omits it.
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' });
}
});
|