fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings: * 38 client components / 56 toast.error sites converted to toastError(err) so the new admin error inspector becomes usable from user-reported issues — every failed inline-edit, save, send, archive, upload, etc. now carries the request-id + error-code (Copy ID action). * 26 service files / 62 bare-Error throws converted to CodedError or the existing AppError subclasses. Adds new error codes: DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502), DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502), IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502), UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for post-insert returning-empty guards. * Five vitest assertions updated to match the new user-facing wording (client-merge "already been merged", expense/interest "couldn't find that …", documenso "signing service didn't respond"). Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1) + MED §11 (auditor-G Issue 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { env } from '@/lib/env';
|
||||
import { CodedError } from '@/lib/errors';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getPortDocumensoConfig, type DocumensoApiVersion } from '@/lib/services/port-config';
|
||||
import { fetchWithTimeout } from '@/lib/fetch-with-timeout';
|
||||
import { fetchWithTimeout, FetchTimeoutError } from '@/lib/fetch-with-timeout';
|
||||
|
||||
interface DocumensoCreds {
|
||||
baseUrl: string;
|
||||
@@ -27,19 +28,36 @@ async function documensoFetch(
|
||||
portId?: string,
|
||||
): Promise<unknown> {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof FetchTimeoutError) {
|
||||
throw new CodedError('DOCUMENSO_TIMEOUT', {
|
||||
internalMessage: `${path} timed out after ${err.timeoutMs}ms`,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
logger.error({ path, status: res.status, err, portId }, 'Documenso API error');
|
||||
throw new Error(`Documenso API error: ${res.status}`);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `${path} → ${res.status}`,
|
||||
});
|
||||
}
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `${path} → ${res.status}: ${err}`,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json();
|
||||
@@ -242,14 +260,32 @@ export async function sendReminder(
|
||||
|
||||
export async function downloadSignedPdf(docId: string, portId?: string): Promise<Buffer> {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v1/documents/${docId}/download`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
const path = `/api/v1/documents/${docId}/download`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof FetchTimeoutError) {
|
||||
throw new CodedError('DOCUMENSO_TIMEOUT', {
|
||||
internalMessage: `${path} timed out after ${err.timeoutMs}ms`,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
logger.error({ docId, status: res.status, err, portId }, 'Documenso download error');
|
||||
throw new Error(`Documenso download error: ${res.status}`);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `${path} → ${res.status}`,
|
||||
});
|
||||
}
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `${path} → ${res.status}: ${err}`,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
@@ -367,7 +403,14 @@ export async function placeFields(
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
logger.error({ docId, status: res.status, err, portId }, 'Documenso v2 placeFields error');
|
||||
throw new Error(`Documenso v2 placeFields error: ${res.status}`);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `v2 placeFields ${docId} → ${res.status}`,
|
||||
});
|
||||
}
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `v2 placeFields ${docId} → ${res.status}: ${err}`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -414,7 +457,14 @@ export async function placeFields(
|
||||
{ docId, status: lastError.status, err: lastError.body, portId },
|
||||
'Documenso v1 placeField error',
|
||||
);
|
||||
throw new Error(`Documenso v1 placeField error: ${lastError.status}`);
|
||||
if (lastError.status === 401 || lastError.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `v1 placeField ${docId} → ${lastError.status}`,
|
||||
});
|
||||
}
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `v1 placeField ${docId} → ${lastError.status}: ${lastError.body}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +532,13 @@ export async function voidDocument(docId: string, portId?: string): Promise<void
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
logger.error({ docId, status: res.status, err, portId }, 'Documenso voidDocument error');
|
||||
throw new Error(`Documenso voidDocument error: ${res.status}`);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
throw new CodedError('DOCUMENSO_AUTH_FAILURE', {
|
||||
internalMessage: `voidDocument ${docId} → ${res.status}`,
|
||||
});
|
||||
}
|
||||
throw new CodedError('DOCUMENSO_UPSTREAM_ERROR', {
|
||||
internalMessage: `voidDocument ${docId} → ${res.status}: ${err}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user