// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ChatServiceInactive, IoError, RateLimitChallengeError, RequestUnauthorizedError as LibsignalRequestUnauthorizedError, ServiceIdNotFound, UntrustedIdentityError, } from '@signalapp/libsignal-client'; import { parseRetryAfter } from '../util/parseRetryAfter.std.ts'; import type { ServiceIdString } from '../types/ServiceId.std.ts'; import { HTTPError } from '../types/HTTPError.std.ts'; import type { HeaderListType } from '../types/WebAPI.d.ts'; import type { CallbackResultType } from './Types.d.ts'; function appendStack(newError: Error, originalError: Error) { // oxlint-disable-next-line no-param-reassign newError.stack += `\nOriginal stack:\n${originalError.stack}`; } class ReplayableError extends Error { functionCode?: number; constructor(options: { name?: string; message: string; functionCode?: number; cause?: unknown; }) { super(options.message, { cause: options.cause }); this.name = options.name || 'ReplayableError'; this.message = options.message; // Maintains proper stack trace, where our error was thrown (only available on V8) // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error if (Error.captureStackTrace) { Error.captureStackTrace(this); } this.functionCode = options.functionCode; } } // oxlint-disable-next-line max-classes-per-file export class SessionNotAllowedToRequestCodeError extends ReplayableError { constructor(message: string) { super({ name: 'SessionNotAllowedToRequestCodeError', message }); } } export class SessionNotVerifiedError extends ReplayableError { constructor(message: string) { super({ name: 'SessionNotVerifiedError', message }); } } export class OutgoingIdentityKeyError extends ReplayableError { public readonly identifier: string; // Note: Data to resend message is no longer captured constructor(incomingIdentifier: string, cause?: UntrustedIdentityError) { const [identifier] = incomingIdentifier.split('.'); super({ name: 'OutgoingIdentityKeyError', message: `The identity of ${identifier} has changed.`, cause, }); this.identifier = identifier; } } export class OutgoingMessageError extends ReplayableError { readonly identifier: string; readonly error?: Error; // Note: Data to resend message is no longer captured constructor(incomingIdentifier: string, error?: Error) { const [identifier] = incomingIdentifier.split('.', 1); super({ name: 'OutgoingMessageError', message: error ? error.message : 'no http error', }); this.identifier = identifier; if (error) { this.error = error; appendStack(this, error); } } get code(): undefined | number { return this.error && 'code' in this.error && typeof this.error.code === 'number' ? this.error.code : undefined; } } export class SendMessageNetworkError extends ReplayableError { readonly identifier: string; readonly code: number; readonly error: HTTPError | IoError | ChatServiceInactive; constructor( identifier: string, error: HTTPError | IoError | ChatServiceInactive ) { super({ name: 'SendMessageNetworkError', message: error.message, }); const [id] = identifier.split('.', 1); this.identifier = id; this.error = error; if (error instanceof HTTPError) { this.code = error.code; } else { this.code = -1; } appendStack(this, error); } } export type SendMessageChallengeData = Readonly<{ token?: string; options?: ReadonlyArray; }>; export class SendMessageChallengeError extends ReplayableError { public readonly identifier: string; public readonly code = 428; public readonly error: HTTPError | RateLimitChallengeError; public readonly data: SendMessageChallengeData | undefined; public readonly retryAt?: number; public readonly responseHeaders: HeaderListType | undefined; constructor(identifier: string, error: HTTPError | RateLimitChallengeError) { super({ name: 'SendMessageChallengeError', message: error.message, cause: error, }); const [id] = identifier.split('.', 1); this.identifier = id; this.error = error; if (error instanceof HTTPError) { this.responseHeaders = error.responseHeaders; this.data = error.response as SendMessageChallengeData; const retryAfter = parseRetryAfter(error.responseHeaders['retry-after']); if (retryAfter) { this.retryAt = Date.now() + retryAfter; } } else { this.data = { token: error.token, options: [...error.options], }; if (error.retryAfterSecs != null) { this.retryAt = Date.now() + error.retryAfterSecs * 1000; } } appendStack(this, error); } } export class SendMessageProtoError extends Error implements CallbackResultType { public readonly successfulServiceIds?: Array; public readonly failoverServiceIds?: Array; public readonly errors?: CallbackResultType['errors']; public readonly unidentifiedDeliveries?: Array; public readonly dataMessage: Uint8Array | undefined; public readonly editMessage: Uint8Array | undefined; // Fields necessary for send log save public readonly contentHint?: number; public readonly contentProto?: Uint8Array; public readonly timestamp?: number; public readonly recipients?: Record>; public readonly sendIsNotFinal?: boolean; constructor({ successfulServiceIds, failoverServiceIds, errors, unidentifiedDeliveries, dataMessage, editMessage, contentHint, contentProto, timestamp, recipients, sendIsNotFinal, }: CallbackResultType) { super(`SendMessageProtoError: ${SendMessageProtoError.getMessage(errors)}`); this.successfulServiceIds = successfulServiceIds; this.failoverServiceIds = failoverServiceIds; this.errors = errors; this.unidentifiedDeliveries = unidentifiedDeliveries; this.dataMessage = dataMessage; this.editMessage = editMessage; this.contentHint = contentHint; this.contentProto = contentProto; this.timestamp = timestamp; this.recipients = recipients; this.sendIsNotFinal = sendIsNotFinal; } protected static getMessage(errors: CallbackResultType['errors']): string { if (!errors) { return 'No errors'; } return errors.map(error => error.toString()).join(', '); } } export class UnregisteredUserError extends Error { readonly serviceId: string; readonly code: number; readonly error: HTTPError | ServiceIdNotFound; constructor( serviceId: ServiceIdString, error: HTTPError | ServiceIdNotFound ) { const { message } = error; super(message); this.message = message; this.name = 'UnregisteredUserError'; // Maintains proper stack trace, where our error was thrown (only available on V8) // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error if (Error.captureStackTrace) { Error.captureStackTrace(this); } this.serviceId = serviceId; this.error = error; if (error instanceof HTTPError) { this.code = error.code; } else { this.code = 404; } appendStack(this, error); } } export class UnauthorizedMessageSendError extends Error { readonly serviceId: ServiceIdString; constructor( serviceId: ServiceIdString, error: HTTPError | LibsignalRequestUnauthorizedError ) { super(error.message, { cause: error }); this.serviceId = serviceId; } } type MismatchedDevicesEntry = { serviceId: ServiceIdString; staleDevices: Array; missingDevices: Array; extraDevices: Array; }; export class MismatchedDevicesError extends Error { readonly entries: Array; constructor(entries: Array) { super('MismatchedDevicesError'); this.entries = entries; } } export class ConnectTimeoutError extends Error {} export class UnknownRecipientError extends Error {} export class IncorrectSenderKeyAuthError extends Error {} export class WarnOnlyError extends Error {} export class NoSenderKeyError extends Error {}