371 lines
11 KiB
TypeScript
371 lines
11 KiB
TypeScript
export type AnalysisErrorTag =
|
|
| "AnalysisProtocolError"
|
|
| "AnalysisInputError"
|
|
| "AnalysisOutputError"
|
|
| "AnalysisCapabilityUnavailableError"
|
|
| "AnalysisCancelledError"
|
|
| "AnalysisTimeoutError"
|
|
| "ProviderSelectionError"
|
|
| "ProviderAdapterError"
|
|
| "ArtifactOperationError"
|
|
| "ProcessCaptureError"
|
|
| "EvidenceIntegrityError"
|
|
| "EvidenceLimitError"
|
|
| "EvidenceFileError"
|
|
| "UnknownRegistryError"
|
|
| "HopperTimeoutError"
|
|
| "HopperCancelledError"
|
|
| "HopperProtocolError"
|
|
| "HopperRemoteError"
|
|
| "HopperProcessError"
|
|
| "HopperStartError"
|
|
| "ConfigurationError"
|
|
| "NoBinaryOpenError"
|
|
| "BinaryTargetError";
|
|
|
|
/** Base class for expected analysis, provider, and session failures. */
|
|
export abstract class AnalysisError extends Error {
|
|
abstract readonly _tag: AnalysisErrorTag;
|
|
}
|
|
|
|
/** Base class for failures produced specifically by the Hopper provider. */
|
|
export abstract class HopperError extends AnalysisError {}
|
|
|
|
/** Provider-neutral invalid analysis input or output at an application boundary. */
|
|
export class AnalysisProtocolError extends AnalysisError {
|
|
readonly _tag = "AnalysisProtocolError";
|
|
}
|
|
|
|
/** Caller input failed provider-neutral application parsing. */
|
|
export class AnalysisInputError extends AnalysisError {
|
|
readonly _tag = "AnalysisInputError";
|
|
|
|
constructor(
|
|
readonly operation: string,
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Invalid analysis input for ${operation}`, options);
|
|
}
|
|
}
|
|
|
|
/** Provider output failed provider-neutral application parsing. */
|
|
export class AnalysisOutputError extends AnalysisError {
|
|
readonly _tag = "AnalysisOutputError";
|
|
|
|
constructor(
|
|
readonly operation: string,
|
|
readonly reason: string,
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Invalid analysis output for ${operation}: ${reason}`, options);
|
|
}
|
|
}
|
|
|
|
/** Selected provider cannot execute a declared analysis operation. */
|
|
export class AnalysisCapabilityUnavailableError extends AnalysisError {
|
|
readonly _tag = "AnalysisCapabilityUnavailableError";
|
|
|
|
constructor(
|
|
readonly providerId: string,
|
|
readonly operation: string,
|
|
readonly reason: string,
|
|
) {
|
|
super(`Provider ${providerId} cannot execute ${operation}: ${reason}`);
|
|
}
|
|
}
|
|
|
|
/** Caller cancellation won before provider-neutral work completed. */
|
|
export class AnalysisCancelledError extends AnalysisError {
|
|
readonly _tag = "AnalysisCancelledError";
|
|
|
|
constructor(readonly operation: string) {
|
|
super(`Analysis operation was cancelled: ${operation}`);
|
|
}
|
|
}
|
|
|
|
/** Provider-neutral operation exceeded its declared execution deadline. */
|
|
export class AnalysisTimeoutError extends AnalysisError {
|
|
readonly _tag = "AnalysisTimeoutError";
|
|
|
|
constructor(
|
|
readonly operation: string,
|
|
readonly timeoutMs: number,
|
|
) {
|
|
super(
|
|
`Analysis operation timed out after ${String(timeoutMs)}ms: ${operation}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** No configured provider can satisfy a requested operation. */
|
|
export class ProviderSelectionError extends AnalysisError {
|
|
readonly _tag = "ProviderSelectionError";
|
|
|
|
constructor(readonly operation: string) {
|
|
super(`No configured provider can execute ${operation}`);
|
|
}
|
|
}
|
|
|
|
/** A provider adapter failed outside its more precise typed variants. */
|
|
export class ProviderAdapterError extends AnalysisError {
|
|
readonly _tag = "ProviderAdapterError";
|
|
|
|
constructor(
|
|
readonly providerId: string,
|
|
readonly operation: string,
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Provider ${providerId} adapter failed during ${operation}`, options);
|
|
}
|
|
}
|
|
|
|
/** Artifact inventory or extraction failed a typed safety boundary. */
|
|
export class ArtifactOperationError extends AnalysisError {
|
|
readonly _tag = "ArtifactOperationError";
|
|
|
|
constructor(
|
|
readonly operation: "inventory_artifact" | "extract_artifact",
|
|
readonly reason:
|
|
| "cancelled"
|
|
| "format"
|
|
| "integrity"
|
|
| "limit"
|
|
| "path"
|
|
| "unavailable"
|
|
| "io",
|
|
) {
|
|
super(`Artifact ${operation} failed: ${reason}`);
|
|
}
|
|
}
|
|
|
|
/** Evidence identity, schema, or bundle manifests failed integrity checks. */
|
|
export class EvidenceIntegrityError extends AnalysisError {
|
|
readonly _tag = "EvidenceIntegrityError";
|
|
}
|
|
|
|
/** A bounded evidence ledger cannot accept more records or serialized bytes. */
|
|
export class EvidenceLimitError extends AnalysisError {
|
|
readonly _tag = "EvidenceLimitError";
|
|
|
|
constructor(
|
|
readonly limit: "records" | "bytes",
|
|
readonly maximum: number,
|
|
) {
|
|
super(`Evidence ledger ${limit} limit exceeded (${String(maximum)})`);
|
|
}
|
|
}
|
|
|
|
/** Evidence bundle filesystem access failed within the configured policy. */
|
|
export class EvidenceFileError extends AnalysisError {
|
|
readonly _tag = "EvidenceFileError";
|
|
|
|
constructor(
|
|
readonly operation: "read" | "write",
|
|
readonly reason:
|
|
| "disabled"
|
|
| "outside-root"
|
|
| "not-file"
|
|
| "too-large"
|
|
| "exists"
|
|
| "invalid-json"
|
|
| "io",
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Evidence bundle ${operation} failed: ${reason}`, options);
|
|
}
|
|
}
|
|
|
|
/** Residual-unknown mutation failed a lifecycle, reference, or CAS invariant. */
|
|
export class UnknownRegistryError extends AnalysisError {
|
|
readonly _tag = "UnknownRegistryError";
|
|
|
|
constructor(
|
|
readonly reason:
|
|
| "not-found"
|
|
| "already-exists"
|
|
| "revision-conflict"
|
|
| "invalid-transition"
|
|
| "integrity"
|
|
| "limit",
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Residual unknown registry mutation failed: ${reason}`, options);
|
|
}
|
|
}
|
|
|
|
/** Hopper did not respond within the configured operation deadline. */
|
|
export class HopperTimeoutError extends HopperError {
|
|
readonly _tag = "HopperTimeoutError";
|
|
|
|
constructor(readonly timeoutMs: number) {
|
|
super(`Hopper did not respond within ${String(timeoutMs)}ms`);
|
|
}
|
|
}
|
|
|
|
/** The caller cancelled a Hopper operation before it completed. */
|
|
export class HopperCancelledError extends HopperError {
|
|
readonly _tag = "HopperCancelledError";
|
|
|
|
constructor() {
|
|
super("Hopper operation was cancelled");
|
|
}
|
|
}
|
|
|
|
/** Hopper returned bytes or JSON that do not satisfy the NDJSON RPC contract. */
|
|
export class HopperProtocolError extends HopperError {
|
|
readonly _tag = "HopperProtocolError";
|
|
}
|
|
|
|
export type HopperDiagnosticType =
|
|
| "remote"
|
|
| "authorization"
|
|
| "invalid_request"
|
|
| "bridge_exception";
|
|
|
|
/** Hopper's JSON-RPC endpoint returned an expected remote error response. */
|
|
export class HopperRemoteError extends HopperError {
|
|
readonly _tag = "HopperRemoteError";
|
|
|
|
constructor(
|
|
readonly code: number,
|
|
readonly safeMessage: string,
|
|
readonly diagnosticType: HopperDiagnosticType = "remote",
|
|
) {
|
|
super(`Hopper request failed (${String(code)}): ${safeMessage}`);
|
|
}
|
|
}
|
|
|
|
/** The owned Hopper bridge stopped before its client was closed. */
|
|
export class HopperProcessError extends HopperError {
|
|
readonly _tag = "HopperProcessError";
|
|
|
|
constructor(readonly exitCode: number | null) {
|
|
super(`Hopper bridge stopped unexpectedly with code ${String(exitCode)}`);
|
|
}
|
|
}
|
|
|
|
/** Hopper or the repository bridge could not be started. */
|
|
export class HopperStartError extends HopperError {
|
|
readonly _tag = "HopperStartError";
|
|
|
|
constructor(options?: ErrorOptions) {
|
|
super("Hopper application bridge could not be started", options);
|
|
}
|
|
}
|
|
|
|
/** Runtime configuration could not be parsed safely. */
|
|
export class ConfigurationError extends AnalysisError {
|
|
readonly _tag = "ConfigurationError";
|
|
}
|
|
|
|
/** No app or binary session exists for an analysis request. */
|
|
export class NoBinaryOpenError extends AnalysisError {
|
|
readonly _tag = "NoBinaryOpenError";
|
|
constructor() {
|
|
super(
|
|
"No app is open. Ask the user which app to investigate, then call open_binary with its local path.",
|
|
);
|
|
}
|
|
}
|
|
|
|
/** A supplied target path could not be safely opened as a supported app or binary. */
|
|
export class BinaryTargetError extends AnalysisError {
|
|
readonly _tag = "BinaryTargetError";
|
|
constructor(
|
|
readonly path: string,
|
|
reason: string,
|
|
options?: ErrorOptions,
|
|
) {
|
|
super(`Cannot open artifact: ${reason}`, options);
|
|
}
|
|
}
|
|
|
|
export interface AnalysisErrorProjection {
|
|
readonly tag: AnalysisErrorTag;
|
|
readonly message: string;
|
|
readonly details: Readonly<Record<string, string | number | null>>;
|
|
}
|
|
|
|
/** Project expected failures into exhaustive, secret-safe caller fields. */
|
|
export const projectAnalysisError = (
|
|
error: AnalysisError,
|
|
): AnalysisErrorProjection => {
|
|
assertKnownTag(error._tag);
|
|
return {
|
|
tag: error._tag,
|
|
message: error.message,
|
|
details: safeDetails(error),
|
|
};
|
|
};
|
|
|
|
const safeDetails = (
|
|
error: AnalysisError,
|
|
): Readonly<Record<string, string | number | null>> => {
|
|
if (error instanceof AnalysisInputError)
|
|
return { operation: error.operation };
|
|
if (error instanceof AnalysisOutputError)
|
|
return { operation: error.operation, reason: error.reason };
|
|
if (error instanceof AnalysisCapabilityUnavailableError)
|
|
return {
|
|
providerId: error.providerId,
|
|
operation: error.operation,
|
|
reason: error.reason,
|
|
};
|
|
if (error instanceof AnalysisCancelledError)
|
|
return { operation: error.operation };
|
|
if (error instanceof AnalysisTimeoutError)
|
|
return { operation: error.operation, timeoutMs: error.timeoutMs };
|
|
if (error instanceof ProviderSelectionError)
|
|
return { operation: error.operation };
|
|
if (error instanceof ProviderAdapterError)
|
|
return { providerId: error.providerId, operation: error.operation };
|
|
if (error instanceof ArtifactOperationError)
|
|
return { operation: error.operation, reason: error.reason };
|
|
if (error instanceof EvidenceLimitError)
|
|
return { limit: error.limit, maximum: error.maximum };
|
|
if (error instanceof EvidenceFileError)
|
|
return { operation: error.operation, reason: error.reason };
|
|
if (error instanceof UnknownRegistryError) return { reason: error.reason };
|
|
if (error instanceof HopperTimeoutError)
|
|
return { timeoutMs: error.timeoutMs };
|
|
if (error instanceof HopperRemoteError)
|
|
return {
|
|
code: error.code,
|
|
safeMessage: error.safeMessage,
|
|
diagnosticType: error.diagnosticType,
|
|
};
|
|
if (error instanceof HopperProcessError) return { exitCode: error.exitCode };
|
|
return {};
|
|
};
|
|
|
|
const KNOWN_ERROR_TAGS = {
|
|
AnalysisProtocolError: true,
|
|
AnalysisInputError: true,
|
|
AnalysisOutputError: true,
|
|
AnalysisCapabilityUnavailableError: true,
|
|
AnalysisCancelledError: true,
|
|
AnalysisTimeoutError: true,
|
|
ProviderSelectionError: true,
|
|
ProviderAdapterError: true,
|
|
ArtifactOperationError: true,
|
|
ProcessCaptureError: true,
|
|
EvidenceIntegrityError: true,
|
|
EvidenceLimitError: true,
|
|
EvidenceFileError: true,
|
|
UnknownRegistryError: true,
|
|
HopperTimeoutError: true,
|
|
HopperCancelledError: true,
|
|
HopperProtocolError: true,
|
|
HopperRemoteError: true,
|
|
HopperProcessError: true,
|
|
HopperStartError: true,
|
|
ConfigurationError: true,
|
|
NoBinaryOpenError: true,
|
|
BinaryTargetError: true,
|
|
} as const satisfies Readonly<Record<AnalysisErrorTag, true>>;
|
|
|
|
const assertKnownTag = (tag: AnalysisErrorTag): void => {
|
|
if (KNOWN_ERROR_TAGS[tag] !== true)
|
|
throw new TypeError("Unknown analysis error tag");
|
|
};
|