83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
interface UspsToken {
|
|
accessToken: string;
|
|
expiresAt: number;
|
|
}
|
|
|
|
let cachedToken: UspsToken | null = null;
|
|
|
|
const USPS_BASE =
|
|
process.env.USPS_ENV === 'production'
|
|
? 'https://apis.usps.com'
|
|
: 'https://apis-tem.usps.com';
|
|
|
|
export function uspsConfigured(): boolean {
|
|
return !!(process.env.USPS_CLIENT_ID && process.env.USPS_CLIENT_SECRET);
|
|
}
|
|
|
|
async function getToken(): Promise<string> {
|
|
if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) {
|
|
return cachedToken.accessToken;
|
|
}
|
|
const resp = await fetch(`${USPS_BASE}/oauth2/v3/token`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
client_id: process.env.USPS_CLIENT_ID,
|
|
client_secret: process.env.USPS_CLIENT_SECRET,
|
|
grant_type: 'client_credentials',
|
|
scope: 'addresses'
|
|
})
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok) throw new Error('USPS auth failed: ' + JSON.stringify(data));
|
|
cachedToken = {
|
|
accessToken: data.access_token,
|
|
expiresAt: Date.now() + (parseInt(data.expires_in, 10) || 3000) * 1000
|
|
};
|
|
return cachedToken.accessToken;
|
|
}
|
|
|
|
export interface AddressInput {
|
|
addressLine1: string;
|
|
addressLine2?: string | null;
|
|
city: string;
|
|
state: string;
|
|
postalCode: string;
|
|
}
|
|
|
|
export interface UspsValidationResult {
|
|
validated: boolean;
|
|
deliverable: boolean | null;
|
|
standardized: Record<string, unknown> | null;
|
|
reason?: string;
|
|
}
|
|
|
|
// Only validates domestic US addresses — USPS Addresses API has no international coverage.
|
|
export async function validateAddress(address: AddressInput): Promise<UspsValidationResult> {
|
|
if (!uspsConfigured()) {
|
|
return { validated: false, deliverable: null, standardized: null, reason: 'USPS not configured' };
|
|
}
|
|
try {
|
|
const token = await getToken();
|
|
const params = new URLSearchParams({
|
|
streetAddress: address.addressLine1,
|
|
city: address.city,
|
|
state: address.state,
|
|
ZIPCode: address.postalCode
|
|
});
|
|
if (address.addressLine2) params.set('secondaryAddress', address.addressLine2);
|
|
|
|
const resp = await fetch(`${USPS_BASE}/addresses/v3/address?${params.toString()}`, {
|
|
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok) {
|
|
return { validated: false, deliverable: null, standardized: null, reason: data.error?.message || 'USPS lookup failed' };
|
|
}
|
|
const deliverable = data.additionalInfo?.DPVConfirmation === 'Y';
|
|
return { validated: true, deliverable, standardized: data };
|
|
} catch (err) {
|
|
return { validated: false, deliverable: null, standardized: null, reason: (err as Error).message };
|
|
}
|
|
}
|