diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f64196f4fc..12eb20466c 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; type CaptchaResponse = { success: boolean; @@ -35,7 +36,7 @@ export class CaptchaService { }, { throwErrorWhenResponseNotOk: false }); if (!res.ok) { - throw new Error(`${res.status}`); + throw ErrorHandling(`${res.status}`); } return await res.json() as CaptchaResponse; @@ -44,48 +45,48 @@ export class CaptchaService { @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new Error('recaptcha-failed: no response provided'); + throw ErrorHandling('recaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw new Error(`recaptcha-request-failed: ${err}`); + throw ErrorHandling(`recaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`recaptcha-failed: ${errorCodes}`); + throw ErrorHandling(`recaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new Error('hcaptcha-failed: no response provided'); + throw ErrorHandling('hcaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw new Error(`hcaptcha-request-failed: ${err}`); + throw ErrorHandling(`hcaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`hcaptcha-failed: ${errorCodes}`); + throw ErrorHandling(`hcaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw new Error('turnstile-failed: no response provided'); + throw ErrorHandling('turnstile-failed: no response provided'); } const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw new Error(`turnstile-request-failed: ${err}`); + throw ErrorHandling(`turnstile-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw new Error(`turnstile-failed: ${errorCodes}`); + throw ErrorHandling(`turnstile-failed: ${errorCodes}`); } } } diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 5eece8cd46..5ba301d2d8 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -16,6 +16,7 @@ import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { DI } from '@/di-symbols.js'; import generateNativeUserToken from '@/misc/generate-native-user-token.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class CreateSystemUserService { @@ -49,7 +50,7 @@ export class CreateSystemUserService { host: IsNull(), }); - if (exist) throw new Error('the user is already exists'); + if (exist) throw ErrorHandling('the user is already exists'); account = await transactionalEntityManager.insert(User, { id: this.idService.genId(), diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index f24a880914..478d7e416d 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -18,6 +18,7 @@ import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/server/api/stream/types.js'; +import { ErrorHandling } from '@/error.js'; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @@ -107,7 +108,7 @@ export class CustomEmojiService implements OnApplicationShutdown { }): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); - if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists'); + if (sameNameEmoji != null && sameNameEmoji.id !== id) throw ErrorHandling('name already exists'); await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 8c5c41ca01..caaeb95445 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -42,6 +42,7 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; +import { ErrorHandling } from '@/error.js'; type AddFileArgs = { /** User who wish to add file */ @@ -535,7 +536,7 @@ export class DriveService { userId: user ? user.id : IsNull(), }); - if (driveFolder == null) throw new Error('folder-not-found'); + if (driveFolder == null) throw ErrorHandling('folder-not-found'); return driveFolder; }; @@ -750,9 +751,13 @@ export class DriveService { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); return; } else { - throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, { - cause: err, - }); + const error = new Error(`Failed to delete the file from the object storage with the given key: ${key}`); + if (process.env.NODE_ENV === 'production') { + Object.defineProperty(error, 'stack', { value: ''}); + Object.defineProperty(err, 'stack', { value: ''}); + } + error['cause'] = err; + throw error; } } } diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 186fd36b42..11da4c74d2 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -13,6 +13,7 @@ import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId, parseObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; import { parseUlid } from '@/misc/id/ulid.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class IdService { @@ -35,7 +36,7 @@ export class IdService { case 'meidg': return genMeidg(date); case 'ulid': return ulid(date.getTime()); case 'objectid': return genObjectId(date); - default: throw new Error('unrecognized id generation method'); + default: throw ErrorHandling('unrecognized id generation method'); } } @@ -47,7 +48,7 @@ export class IdService { case 'meid': return parseMeid(id); case 'meidg': return parseMeidg(id); case 'ulid': return parseUlid(id); - default: throw new Error('unrecognized id generation method'); + default: throw ErrorHandling('unrecognized id generation method'); } } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 001947322d..fa1604b1d8 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -53,6 +53,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; +import { ErrorHandling } from '@/error.js'; const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); @@ -251,7 +252,7 @@ export class NoteCreateService implements OnApplicationShutdown { // Renote対象が「ホームまたは全体」以外の公開範囲ならreject if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); + throw ErrorHandling('Renote target is not public or home'); } // Renote対象がpublicではないならhomeにする @@ -316,7 +317,7 @@ export class NoteCreateService implements OnApplicationShutdown { } if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); + if (data.visibleUsers == null) throw ErrorHandling('invalid param'); for (const u of data.visibleUsers) { if (!mentionedUsers.some(x => x.id === u.id)) { @@ -436,6 +437,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (isDuplicateKeyValueError(e)) { const err = new Error('Duplicated note'); err.name = 'duplicated'; + if (process.env.NODE_ENV === 'production') { + Object.defineProperty(err, 'stack', { value: ''}); + } throw err; } @@ -525,7 +529,7 @@ export class NoteCreateService implements OnApplicationShutdown { // 未読通知を作成 if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); + if (data.visibleUsers == null) throw ErrorHandling('invalid param'); for (const u of data.visibleUsers) { // ローカルユーザーのみ diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index f317087b41..d0327dcdbe 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class PollService { @@ -45,16 +46,16 @@ export class PollService { public async vote(user: User, note: Note, choice: number) { const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('poll not found'); + if (poll == null) throw ErrorHandling('poll not found'); // Check whether is valid choice - if (poll.choices[choice] == null) throw new Error('invalid choice param'); + if (poll.choices[choice] == null) throw ErrorHandling('invalid choice param'); // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); if (blocked) { - throw new Error('blocked'); + throw ErrorHandling('blocked'); } } @@ -66,10 +67,10 @@ export class PollService { if (poll.multiple) { if (exist.some(x => x.choice === choice)) { - throw new Error('already voted'); + throw ErrorHandling('already voted'); } } else if (exist.length !== 0) { - throw new Error('already voted'); + throw ErrorHandling('already voted'); } // Create vote @@ -94,10 +95,10 @@ export class PollService { @bindThis public async deliverQuestionUpdate(noteId: Note['id']) { const note = await this.notesRepository.findOneBy({ id: noteId }); - if (note == null) throw new Error('note not found'); + if (note == null) throw ErrorHandling('note not found'); const user = await this.usersRepository.findOneBy({ id: note.userId }); - if (user == null) throw new Error('note not found'); + if (user == null) throw ErrorHandling('note not found'); if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user)); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 1ac906991b..13fc24092d 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -16,6 +16,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -74,7 +75,7 @@ export class RelayService { }); if (relay == null) { - throw new Error('relay not found'); + throw ErrorHandling('relay not found'); } const relayActor = await this.getRelayActor(); diff --git a/packages/backend/src/core/RemoteUserResolveService.ts b/packages/backend/src/core/RemoteUserResolveService.ts index 31682ea98d..d4dd9c4ff9 100644 --- a/packages/backend/src/core/RemoteUserResolveService.ts +++ b/packages/backend/src/core/RemoteUserResolveService.ts @@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class RemoteUserResolveService { @@ -47,7 +48,7 @@ export class RemoteUserResolveService { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { - throw new Error('user not found'); + throw ErrorHandling('user not found'); } else { return u; } @@ -60,7 +61,7 @@ export class RemoteUserResolveService { this.logger.info(`return local user: ${usernameLower}`); return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => { if (u == null) { - throw new Error('user not found'); + throw ErrorHandling('user not found'); } else { return u; } @@ -82,7 +83,7 @@ export class RemoteUserResolveService { .getUserFromApId(self.href) .then((u) => { if (u == null) { - throw new Error('local user not found'); + throw ErrorHandling('local user not found'); } else { return u; } @@ -112,7 +113,7 @@ export class RemoteUserResolveService { // validate uri const uri = new URL(self.href); if (uri.hostname !== host) { - throw new Error('Invalid uri'); + throw ErrorHandling('Invalid uri'); } await this.usersRepository.update({ @@ -130,7 +131,7 @@ export class RemoteUserResolveService { this.logger.info(`return resynced remote user: ${acctLower}`); return await this.usersRepository.findOneBy({ uri: self.href }).then(u => { if (u == null) { - throw new Error('user not found'); + throw ErrorHandling('user not found'); } else { return u as LocalUser | RemoteUser; } @@ -146,12 +147,12 @@ export class RemoteUserResolveService { this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await this.webfingerService.webfinger(acctLower).catch(err => { this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`); - throw new Error(`Failed to WebFinger for ${acctLower}: ${ err.statusCode ?? err.message }`); + throw ErrorHandling(`Failed to WebFinger for ${acctLower}: ${ err.statusCode ?? err.message }`); }); const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); if (!self) { this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); - throw new Error('self link not found'); + throw ErrorHandling('self link not found'); } return self; } diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 88b368dd22..6b745a3152 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -15,6 +15,7 @@ import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; import type { Index, MeiliSearch } from 'meilisearch'; +import { ErrorHandling } from '@/error.js'; type K = string; type V = string | number | boolean; @@ -37,7 +38,7 @@ function compileValue(value: V): string { } else if (typeof value === 'boolean') { return value.toString(); } - throw new Error('unrecognized value'); + throw ErrorHandling('unrecognized value'); } function compileQuery(q: Q): string { @@ -51,7 +52,7 @@ function compileQuery(q: Q): string { case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`; case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`; case 'not': return `(NOT ${compileQuery(q.q)})`; - default: throw new Error('unrecognized query operator'); + default: throw ErrorHandling('unrecognized query operator'); } } diff --git a/packages/backend/src/core/TwoFactorAuthenticationService.ts b/packages/backend/src/core/TwoFactorAuthenticationService.ts index ecf7676f4b..7329697bff 100644 --- a/packages/backend/src/core/TwoFactorAuthenticationService.ts +++ b/packages/backend/src/core/TwoFactorAuthenticationService.ts @@ -9,6 +9,7 @@ import * as jsrsasign from 'jsrsasign'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; const ECC_PRELUDE = Buffer.from([0x04]); const NULL_BYTE = Buffer.from([0]); @@ -72,7 +73,7 @@ function verifyCertificateChain(certificates: string[]) { const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1]; const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]); - if (certStruct == null) throw new Error('certStruct is null'); + if (certStruct == null) throw ErrorHandling('certStruct is null'); const algorithm = certificate.getSignatureAlgorithmField(); const signatureHex = certificate.getSignatureValueHex(); @@ -142,14 +143,14 @@ export class TwoFactorAuthenticationService { challenge: string }) { if (clientData.type !== 'webauthn.get') { - throw new Error('type is not webauthn.get'); + throw ErrorHandling('type is not webauthn.get'); } if (this.hash(clientData.challenge).toString('hex') !== challenge) { - throw new Error('challenge mismatch'); + throw ErrorHandling('challenge mismatch'); } if (clientData.origin !== this.config.scheme + '://' + this.config.host) { - throw new Error('origin mismatch'); + throw ErrorHandling('origin mismatch'); } const verificationData = Buffer.concat( @@ -171,11 +172,11 @@ export class TwoFactorAuthenticationService { const negTwo = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); + throw ErrorHandling('invalid or no -2 key given'); } const negThree = publicKey.get(-3); if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); + throw ErrorHandling('invalid or no -3 key given'); } const publicKeyU2F = Buffer.concat( @@ -206,7 +207,7 @@ export class TwoFactorAuthenticationService { credentialId: Buffer, }) { if (attStmt.alg !== -7) { - throw new Error('alg mismatch'); + throw ErrorHandling('alg mismatch'); } const verificationData = Buffer.concat([ @@ -219,11 +220,11 @@ export class TwoFactorAuthenticationService { const negTwo = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); + throw ErrorHandling('invalid or no -2 key given'); } const negThree = publicKey.get(-3); if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); + throw ErrorHandling('invalid or no -3 key given'); } const publicKeyData = Buffer.concat( @@ -232,7 +233,7 @@ export class TwoFactorAuthenticationService { ); if (!attCert.equals(publicKeyData)) { - throw new Error('public key mismatch'); + throw ErrorHandling('public key mismatch'); } const isValid = crypto @@ -278,7 +279,7 @@ export class TwoFactorAuthenticationService { const signature = jwsParts[2]; if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) { - throw new Error('invalid nonce'); + throw ErrorHandling('invalid nonce'); } const certificateChain = header.x5c @@ -286,11 +287,11 @@ export class TwoFactorAuthenticationService { .concat([GSR2]); if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') { - throw new Error('invalid common name'); + throw ErrorHandling('invalid common name'); } if (!verifyCertificateChain(certificateChain)) { - throw new Error('Invalid certificate chain!'); + throw ErrorHandling('Invalid certificate chain!'); } const signatureBase = Buffer.from( @@ -306,11 +307,11 @@ export class TwoFactorAuthenticationService { const negTwo = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); + throw ErrorHandling('invalid or no -2 key given'); } const negThree = publicKey.get(-3); if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); + throw ErrorHandling('invalid or no -3 key given'); } const publicKeyData = Buffer.concat( @@ -355,11 +356,11 @@ export class TwoFactorAuthenticationService { const negTwo = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); + throw ErrorHandling('invalid or no -2 key given'); } const negThree = publicKey.get(-3); if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); + throw ErrorHandling('invalid or no -3 key given'); } const publicKeyData = Buffer.concat( @@ -373,11 +374,11 @@ export class TwoFactorAuthenticationService { }; } else if (attStmt.ecdaaKeyId) { // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation - throw new Error('ECDAA-Verify is not supported'); + throw ErrorHandling('ECDAA-Verify is not supported'); } else { - if (attStmt.alg !== -7) throw new Error('alg mismatch'); + if (attStmt.alg !== -7) throw ErrorHandling('alg mismatch'); - throw new Error('self attestation is not supported'); + throw ErrorHandling('self attestation is not supported'); } }, }, @@ -400,7 +401,7 @@ export class TwoFactorAuthenticationService { }) { const x5c: Buffer[] = attStmt.x5c; if (x5c.length !== 1) { - throw new Error('x5c length does not match expectation'); + throw ErrorHandling('x5c length does not match expectation'); } const attCert = x5c[0]; @@ -410,11 +411,11 @@ export class TwoFactorAuthenticationService { const negTwo: Buffer = publicKey.get(-2); if (!negTwo || negTwo.length !== 32) { - throw new Error('invalid or no -2 key given'); + throw ErrorHandling('invalid or no -2 key given'); } const negThree: Buffer = publicKey.get(-3); if (!negThree || negThree.length !== 32) { - throw new Error('invalid or no -3 key given'); + throw ErrorHandling('invalid or no -3 key given'); } const publicKeyU2F = Buffer.concat( diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 8e356d19bb..33aa1e44ab 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -29,6 +29,7 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import Logger from '../logger.js'; +import { ErrorHandling } from '@/error.js'; const logger = new Logger('following/create'); @@ -459,8 +460,8 @@ export class UserFollowingService implements OnModuleInit { this.userBlockingService.checkBlocked(followee.id, follower.id), ]); - if (blocking) throw new Error('blocking'); - if (blocked) throw new Error('blocked'); + if (blocking) throw ErrorHandling('blocking'); + if (blocked) throw ErrorHandling('blocked'); const followRequest = await this.followRequestsRepository.insert({ id: this.idService.genId(), diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index f31d5f84e5..911a020761 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -30,6 +30,7 @@ import { isNotNull } from '@/misc/is-not-null.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class ApRendererService { @@ -98,7 +99,7 @@ export class ApRendererService { to = [`${attributedTo}/followers`]; cc = []; } else { - throw new Error('renderAnnounce: cannot render non-public note'); + throw ErrorHandling('renderAnnounce: cannot render non-public note'); } return { @@ -120,7 +121,7 @@ export class ApRendererService { @bindThis public renderBlock(block: Blocking): IBlock { if (block.blockee?.uri == null) { - throw new Error('renderBlock: missing blockee uri'); + throw ErrorHandling('renderBlock: missing blockee uri'); } return { diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 31bea744e2..ca85a3111b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -20,6 +20,7 @@ import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import { ErrorHandling } from '@/error.js'; export class Resolver { private history: Set; @@ -60,7 +61,7 @@ export class Resolver { if (isCollectionOrOrderedCollection(collection)) { return collection; } else { - throw new Error(`unrecognized collection type: ${collection.type}`); + throw ErrorHandling(`unrecognized collection type: ${collection.type}`); } } @@ -74,15 +75,15 @@ export class Resolver { // URLs with fragment parts cannot be resolved correctly because // the fragment part does not get transmitted over HTTP(S). // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); + throw ErrorHandling(`cannot resolve URL with fragment: ${value}`); } if (this.history.has(value)) { - throw new Error('cannot resolve already resolved one'); + throw ErrorHandling('cannot resolve already resolved one'); } if (this.history.size > this.recursionLimit) { - throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); + throw ErrorHandling(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`); } this.history.add(value); @@ -94,7 +95,7 @@ export class Resolver { const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { - throw new Error('Instance is blocked'); + throw ErrorHandling('Instance is blocked'); } if (this.config.signToActivityPubGet && !this.user) { @@ -110,7 +111,7 @@ export class Resolver { !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' ) { - throw new Error('invalid response'); + throw ErrorHandling('invalid response'); } return object; @@ -119,7 +120,7 @@ export class Resolver { @bindThis private resolveLocal(url: string): Promise { const parsed = this.apDbResolverService.parseUri(url); - if (!parsed.local) throw new Error('resolveLocal: not local'); + if (!parsed.local) throw ErrorHandling('resolveLocal: not local'); switch (parsed.type) { case 'notes': @@ -147,14 +148,14 @@ export class Resolver { this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null }))); case 'follows': // rest should be - if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI'); + if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw ErrorHandling('resolveLocal: invalid follow URI'); return Promise.all( [parsed.id, parsed.rest].map(id => this.usersRepository.findOneByOrFail({ id })), ) .then(([follower, followee]) => this.apRendererService.addContext(this.apRendererService.renderFollow(follower as LocalUser | RemoteUser, followee as LocalUser | RemoteUser, url))); default: - throw new Error(`resolveLocal: type ${parsed.type} unhandled`); + throw ErrorHandling(`resolveLocal: type ${parsed.type} unhandled`); } } } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 41d1bc48a7..fc06515ac5 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -37,6 +37,7 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class ApNoteService { @@ -80,16 +81,28 @@ export class ApNoteService { const expectHost = this.utilityService.extractDbHost(uri); if (!validPost.includes(getApType(object))) { - return new Error(`invalid Note: invalid object type ${getApType(object)}`); + const error = new Error(`invalid Note: invalid object type ${getApType(object)}`); + if (process.env.NODE_ENV === 'production') { + Object.defineProperty(error, 'stack', { value: ''}); + } + return error } if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { - return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + const error = new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); + if (process.env.NODE_ENV === 'production') { + Object.defineProperty(error, 'stack', { value: ''}); + } + return error } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + const error = new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + if (process.env.NODE_ENV === 'production') { + Object.defineProperty(error, 'stack', { value: ''}); + } + return error } return null; @@ -123,7 +136,7 @@ export class ApNoteService { value, object, }); - throw new Error('invalid note'); + throw ErrorHandling('invalid note'); } const note = object as IPost; @@ -131,27 +144,27 @@ export class ApNoteService { this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id && !checkHttps(note.id)) { - throw new Error('unexpected schema of note.id: ' + note.id); + throw ErrorHandling('unexpected schema of note.id: ' + note.id); } const url = getOneApHrefNullable(note.url); if (url && !checkHttps(url)) { - throw new Error('unexpected schema of note url: ' + url); + throw ErrorHandling('unexpected schema of note url: ' + url); } this.logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ if (note.attributedTo == null) { - throw new Error('invalid note.attributedTo: ' + note.attributedTo); + throw ErrorHandling('invalid note.attributedTo: ' + note.attributedTo); } const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as RemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - throw new Error('actor has been suspended'); + throw ErrorHandling('actor has been suspended'); } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); @@ -186,7 +199,7 @@ export class ApNoteService { .then(x => { if (x == null) { this.logger.warn('Specified inReplyTo, but not found'); - throw new Error('inReplyTo not found'); + throw ErrorHandling('inReplyTo not found'); } return x; @@ -223,7 +236,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: Note } => x.status === 'ok').map(x => x.res).at(0); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw new Error('quote resolve failed'); + throw ErrorHandling('quote resolve failed'); } } } @@ -297,7 +310,7 @@ export class ApNoteService { this.logger.info('The note is already inserted while creating itself, reading again'); const duplicate = await this.fetchNote(value); if (!duplicate) { - throw new Error('The note creation failed with duplication error even when there is no duplication'); + throw ErrorHandling('The note creation failed with duplication error even when there is no duplication'); } return duplicate; } @@ -376,7 +389,7 @@ export class ApNoteService { }); const emoji = await this.emojisRepository.findOneBy({ host, name }); - if (emoji == null) throw new Error('emoji update failed'); + if (emoji == null) throw ErrorHandling('emoji update failed'); return emoji; } diff --git a/packages/backend/src/error.ts b/packages/backend/src/error.ts new file mode 100644 index 0000000000..5e85978e26 --- /dev/null +++ b/packages/backend/src/error.ts @@ -0,0 +1,7 @@ +export function ErrorHandling(message: string): Error { + const error = new Error(message); + if (process.env.NODE_ENV === "production") { + error.stack = undefined; + } + return error; +} \ No newline at end of file diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index c16f2f504f..a619abc823 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -11,6 +11,7 @@ import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { Note } from '@/models/entities/Note.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class GetterService { @@ -61,7 +62,7 @@ export class GetterService { const user = await this.getUser(userId); if (!this.userEntityService.isRemoteUser(user)) { - throw new Error('user is not a remote user'); + throw ErrorHandling('user is not a remote user'); } return user; @@ -75,7 +76,7 @@ export class GetterService { const user = await this.getUser(userId); if (!this.userEntityService.isLocalUser(user)) { - throw new Error('user is not a local user'); + throw ErrorHandling('user is not a local user'); } return user; diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index e420a42ab3..25ecf9901c 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { RelayService } from '@/core/RelayService.js'; import { ApiError } from '../../../error.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { tags: ['admin'], @@ -67,7 +68,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { try { - if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only'); + if (new URL(ps.inbox).protocol !== 'https:') throw ErrorHandling('https only'); } catch { throw new ApiError(meta.errors.invalidUrl); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1e8c38c96b..de06024840 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { tags: ['admin'], @@ -53,7 +54,7 @@ export default class extends Endpoint { ]); if (user == null || profile == null) { - throw new Error('user not found'); + throw ErrorHandling('user not found'); } const isModerator = await this.roleService.isModerator(user); @@ -61,7 +62,7 @@ export default class extends Endpoint { const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) { - throw new Error('cannot show info of admin'); + throw ErrorHandling('cannot show info of admin'); } const signins = await this.signinsRepository.findBy({ userId: user.id }); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 8924568440..e82e3b78a5 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -12,6 +12,7 @@ import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { tags: ['antennas'], @@ -87,7 +88,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { if ((ps.keywords.length === 0) || ps.keywords[0].every(x => x === '')) { - throw new Error('invalid param'); + throw ErrorHandling('invalid param'); } const currentAntennasCount = await this.antennasRepository.countBy({ diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts index d7ba21c259..0cf0061487 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts @@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { requireCredential: true, @@ -41,7 +42,7 @@ export default class extends Endpoint { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id }); if (profile.twoFactorTempSecret == null) { - throw new Error('二段階認証の設定が開始されていません'); + throw ErrorHandling('二段階認証の設定が開始されていません'); } const delta = OTPAuth.TOTP.validate({ @@ -52,7 +53,7 @@ export default class extends Endpoint { }); if (delta === null) { - throw new Error('not verified'); + throw ErrorHandling('not verified'); } await this.userProfilesRepository.update(me.id, { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index fd6e70a9de..07d9aff311 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js'; import type { AttestationChallengesRepository, UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js'; +import { ErrorHandling } from '@/error.js'; const cborDecodeFirst = promisify(cbor.decodeFirst) as any; @@ -64,20 +65,20 @@ export default class extends Endpoint { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw ErrorHandling('incorrect password'); } if (!profile.twoFactorEnabled) { - throw new Error('2fa not enabled'); + throw ErrorHandling('2fa not enabled'); } const clientData = JSON.parse(ps.clientDataJSON); if (clientData.type !== 'webauthn.create') { - throw new Error('not a creation attestation'); + throw ErrorHandling('not a creation attestation'); } if (clientData.origin !== this.config.scheme + '://' + this.config.host) { - throw new Error('origin mismatch'); + throw ErrorHandling('origin mismatch'); } const clientDataJSONHash = this.twoFactorAuthenticationService.hash(Buffer.from(ps.clientDataJSON, 'utf-8')); @@ -86,14 +87,14 @@ export default class extends Endpoint { const rpIdHash = attestation.authData.slice(0, 32); if (!rpIdHashReal.equals(rpIdHash)) { - throw new Error('rpIdHash mismatch'); + throw ErrorHandling('rpIdHash mismatch'); } const flags = attestation.authData[32]; // eslint:disable-next-line:no-bitwise if (!(flags & 1)) { - throw new Error('user not present'); + throw ErrorHandling('user not present'); } const authData = Buffer.from(attestation.authData); @@ -102,13 +103,13 @@ export default class extends Endpoint { const publicKeyData = authData.slice(55 + credentialIdLength); const publicKey: Map = await cborDecodeFirst(publicKeyData); if (publicKey.get(3) !== -7) { - throw new Error('alg mismatch'); + throw ErrorHandling('alg mismatch'); } const procedures = this.twoFactorAuthenticationService.getProcedures(); if (!(procedures as any)[attestation.fmt]) { - throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`); + throw ErrorHandling(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`); } const verificationData = (procedures as any)[attestation.fmt].verify({ @@ -119,7 +120,7 @@ export default class extends Endpoint { publicKey, rpIdHash, }); - if (!verificationData.valid) throw new Error('signature invalid'); + if (!verificationData.valid) throw ErrorHandling('signature invalid'); const attestationChallenge = await this.attestationChallengesRepository.findOneBy({ userId: me.id, @@ -129,7 +130,7 @@ export default class extends Endpoint { }); if (!attestationChallenge) { - throw new Error('non-existent challenge'); + throw ErrorHandling('non-existent challenge'); } await this.attestationChallengesRepository.delete({ @@ -142,7 +143,7 @@ export default class extends Endpoint { new Date().getTime() - attestationChallenge.createdAt.getTime() >= 5 * 60 * 1000 ) { - throw new Error('expired challenge'); + throw ErrorHandling('expired challenge'); } const credentialIdString = credentialId.toString('hex'); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 0e57f07e5e..994dbc6f2f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -11,6 +11,7 @@ import type { UserProfilesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { requireCredential: true, @@ -43,7 +44,7 @@ export default class extends Endpoint { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw ErrorHandling('incorrect password'); } // Generate user's secret key diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 6402f3440f..80a58d1158 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -10,6 +10,7 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { requireCredential: true, @@ -46,7 +47,7 @@ export default class extends Endpoint { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw ErrorHandling('incorrect password'); } // Make sure we only delete the user's own creds diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index c0e9ff5ec0..81e840e4b9 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { UserProfilesRepository } from '@/models/index.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { requireCredential: true, @@ -42,7 +43,7 @@ export default class extends Endpoint { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw ErrorHandling('incorrect password'); } await this.userProfilesRepository.update(me.id, { diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index 721eb01c51..3db056302d 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -9,6 +9,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/index.js' import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { requireCredential: true, @@ -47,7 +48,7 @@ export default class extends Endpoint { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw ErrorHandling('incorrect password'); } await this.deleteAccountService.deleteAccount(me); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index dcf3b8772a..96c5c169b0 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -9,6 +9,7 @@ import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { resetDb } from '@/misc/reset-db.js'; +import { ErrorHandling } from '@/error.js'; export const meta = { tags: ['non-productive'], @@ -39,7 +40,7 @@ export default class extends Endpoint { private redisClient: Redis.Redis, ) { super(meta, paramDef, async (ps, me) => { - if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); + if (process.env.NODE_ENV !== 'test') throw ErrorHandling('NODE_ENV is not a test'); await redisClient.flushdb(); await resetDb(this.db); diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts index 4bbd3d0ef1..187998ff05 100644 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/reset-password.ts @@ -8,6 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UserProfilesRepository, PasswordResetRequestsRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { ErrorHandling } from '@/error.js'; // TODO Line 51 export const meta = { tags: ['reset password'], diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 8fd106c10c..8bdd4f7c08 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -19,6 +19,7 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class ChannelsService { @@ -59,7 +60,7 @@ export class ChannelsService { case 'admin': return this.adminChannelService; default: - throw new Error(`no such channel: ${name}`); + throw ErrorHandling(`no such channel: ${name}`); } } } diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 61c89a4258..2bb801b038 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -33,6 +33,7 @@ import Logger from '@/logger.js'; import { StatusError } from '@/misc/status-error.js'; import type { ServerResponse } from 'node:http'; import type { FastifyInstance } from 'fastify'; +import { ErrorHandling } from '@/error.js'; // TODO: Consider migrating to @node-oauth/oauth2-server once // https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. @@ -370,7 +371,7 @@ export class OAuth2ProviderService { fastify.get('/oauth/authorize', async (request, reply) => { const oauth2 = (request.raw as MiddlewareRequest).oauth2; if (!oauth2) { - throw new Error('Unexpected lack of authorization information'); + throw ErrorHandling('Unexpected lack of authorization information'); } this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index ebc1b72761..a5687c74f3 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -42,6 +42,7 @@ import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; import { ClientLoggerService } from './ClientLoggerService.js'; import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; +import { ErrorHandling } from '@/error.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -147,17 +148,17 @@ export class ClientServerService { const token = request.cookies.token; if (token == null) { reply.code(401); - throw new Error('login required'); + throw ErrorHandling('login required'); } const user = await this.usersRepository.findOneBy({ token }); if (user == null) { reply.code(403); - throw new Error('no such user'); + throw ErrorHandling('no such user'); } const isAdministrator = await this.roleService.isAdministrator(user); if (!isAdministrator) { reply.code(403); - throw new Error('access denied'); + throw ErrorHandling('access denied'); } } }); diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d590244e34..0db5c6de56 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { ErrorHandling } from '@/error.js'; @Injectable() export class UrlPreviewService { @@ -84,11 +85,11 @@ export class UrlPreviewService { this.logger.succ(`Got preview of ${url}: ${summary.title}`); if (!(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { - throw new Error('unsupported schema included'); + throw ErrorHandling('unsupported schema included'); } if (summary.player.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) { - throw new Error('unsupported schema included'); + throw ErrorHandling('unsupported schema included'); } summary.icon = this.wrap(summary.icon); diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 1a194ae9db..cc2229a819 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -85,6 +85,7 @@ import { deviceKind } from '@/scripts/device-kind'; import MkButton from '@/components/MkButton.vue'; import { versatileLang } from '@/scripts/intl-const'; import { defaultStore } from '@/store'; +import { ErrorHandling } from '@/error'; type SummalyResult = Awaited>; @@ -124,7 +125,7 @@ let tweetHeight = $ref(150); let unknownUrl = $ref(false); const requestUrl = new URL(props.url); -if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); +if (!['http:', 'https:'].includes(requestUrl.protocol)) throw ErrorHandling('invalid url'); if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com') { const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/); diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 9b9c3d5ec4..64e715ea20 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -27,13 +27,14 @@ SPDX-License-Identifier: AGPL-3.0-only import MkWindow from '@/components/MkWindow.vue'; import { versatileLang } from '@/scripts/intl-const'; import { defaultStore } from '@/store'; +import { ErrorHandling } from '@/error'; const props = defineProps<{ url: string; }>(); const requestUrl = new URL(props.url); -if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); +if (!['http:', 'https:'].includes(requestUrl.protocol)) throw ErrorHandling('invalid url'); let fetching = $ref(true); let title = $ref(null); diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 6bcfe3e1a5..0f76e436e0 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -30,6 +30,7 @@ import { url as local } from '@/config'; import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { safeURIDecode } from '@/scripts/safe-uri-decode'; +import { ErrorHandling } from '@/error'; const props = defineProps<{ url: string; @@ -38,7 +39,7 @@ const props = defineProps<{ const self = props.url.startsWith(local); const url = new URL(props.url); -if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); +if (!['http:', 'https:'].includes(url.protocol)) throw ErrorHandling('invalid url'); const el = ref(); useTooltip(el, (showing) => { diff --git a/packages/frontend/src/error.ts b/packages/frontend/src/error.ts new file mode 100644 index 0000000000..5e85978e26 --- /dev/null +++ b/packages/frontend/src/error.ts @@ -0,0 +1,7 @@ +export function ErrorHandling(message: string): Error { + const error = new Error(message); + if (process.env.NODE_ENV === "production") { + error.stack = undefined; + } + return error; +} \ No newline at end of file diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index 2cc396fc24..82a6eefe02 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -50,6 +50,7 @@ import * as os from '@/os'; import { $i, login } from '@/account'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; +import { ErrorHandling } from '@/error'; const props = defineProps<{ token: string; @@ -62,7 +63,7 @@ function accepted() { state = 'accepted'; if (session && session.app.callbackUrl) { const url = new URL(session.app.callbackUrl); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw ErrorHandling('invalid url'); location.href = `${session.app.callbackUrl}?token=${session.token}`; } } diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue index de3b4a57ac..76c2157f91 100644 --- a/packages/frontend/src/pages/follow.vue +++ b/packages/frontend/src/pages/follow.vue @@ -14,6 +14,7 @@ import * as Acct from 'misskey-js/built/acct'; import * as os from '@/os'; import { mainRouter } from '@/router'; import { i18n } from '@/i18n'; +import { ErrorHandling } from '@/error'; async function follow(user): Promise { const { canceled } = await os.confirm({ @@ -33,7 +34,7 @@ async function follow(user): Promise { const acct = new URL(location.href).searchParams.get('acct'); if (acct == null) { - throw new Error('acct required'); + throw ErrorHandling('acct required'); } let promise; diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 24355c0556..c68c21fa81 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -136,6 +136,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkPagination from '@/components/MkPagination.vue'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; import { dateString } from '@/filters/date'; +import { ErrorHandling } from '@/error'; const props = defineProps<{ host: string; @@ -173,8 +174,8 @@ async function fetch(): Promise { } async function toggleBlock(): Promise { - if (!meta) throw new Error('No meta?'); - if (!instance) throw new Error('No instance?'); + if (!meta) throw ErrorHandling('No meta?'); + if (!instance) throw ErrorHandling('No instance?'); const { host } = instance; await os.api('admin/update-meta', { blockedHosts: isBlocked ? meta.blockedHosts.concat([host]) : meta.blockedHosts.filter(x => x !== host), @@ -182,7 +183,7 @@ async function toggleBlock(): Promise { } async function toggleSuspend(): Promise { - if (!instance) throw new Error('No instance?'); + if (!instance) throw ErrorHandling('No instance?'); await os.api('admin/federation/update-instance', { host: instance.host, isSuspended: suspended, @@ -190,7 +191,7 @@ async function toggleSuspend(): Promise { } function refreshMetadata(): void { - if (!instance) throw new Error('No instance?'); + if (!instance) throw ErrorHandling('No instance?'); os.api('admin/federation/refresh-remote-instance-metadata', { host: instance.host, }); diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 4df40db917..0a392f50a6 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -50,6 +50,7 @@ import * as os from '@/os'; import { $i, login } from '@/account'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { ErrorHandling } from '@/error'; const props = defineProps<{ session: string; @@ -75,7 +76,7 @@ async function accept(): Promise { state = 'accepted'; if (props.callback) { const cbUrl = new URL(props.callback); - if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); + if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw ErrorHandling('invalid url'); cbUrl.searchParams.set('session', props.session); location.href = cbUrl.href; } diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 3b39a5c00a..859051c406 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -185,6 +185,7 @@ import { unisonReload } from '@/scripts/unison-reload'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; +import { ErrorHandling } from '@/error'; const lang = ref(miLocalStorage.getItem('lang')); const fontSize = ref(miLocalStorage.getItem('fontSize')); @@ -276,7 +277,7 @@ function downloadEmojiIndex(lang: string) { function download() { switch (lang) { case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default); - default: throw new Error('unrecognized lang: ' + lang); + default: throw ErrorHandling('unrecognized lang: ' + lang); } } currentIndexes[lang] = await download(); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index eb33cbd3a4..fb5d70af8f 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -51,6 +51,8 @@ import { i18n } from '@/i18n'; import { version, host } from '@/config'; import { definePageMetadata } from '@/scripts/page-metadata'; import { miLocalStorage } from '@/local-storage'; +import { ErrorHandling } from '@/error'; + const { t, ts } = i18n; const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ @@ -133,27 +135,27 @@ function isObject(value: unknown): value is Record { } function validate(profile: any): void { - if (!isObject(profile)) throw new Error('not an object'); + if (!isObject(profile)) throw ErrorHandling('not an object'); // Check if unnecessary properties exist - if (Object.keys(profile).some(key => !profileProps.includes(key))) throw new Error('Unnecessary properties exist'); + if (Object.keys(profile).some(key => !profileProps.includes(key))) throw ErrorHandling('Unnecessary properties exist'); - if (!profile.name) throw new Error('Missing required prop: name'); - if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion'); + if (!profile.name) throw ErrorHandling('Missing required prop: name'); + if (!profile.misskeyVersion) throw ErrorHandling('Missing required prop: misskeyVersion'); // Check if createdAt and updatedAt is Date // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date - if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date'); + if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw ErrorHandling('createdAt is falsy or not Date'); if (profile.updatedAt) { if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) { - throw new Error('updatedAt is not Date'); + throw ErrorHandling('updatedAt is not Date'); } } else if (profile.updatedAt !== null) { - throw new Error('updatedAt is not null'); + throw ErrorHandling('updatedAt is not null'); } - if (!profile.settings) throw new Error('Missing required prop: settings'); - if (!isObject(profile.settings)) throw new Error('Invalid prop: settings'); + if (!profile.settings) throw ErrorHandling('Missing required prop: settings'); + if (!isObject(profile.settings)) throw ErrorHandling('Invalid prop: settings'); } function getSettings(): Profile['settings'] { diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index f0eebf2242..7b63f775ac 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -22,5 +22,5 @@ export function pleaseLogin(path?: string) { }, }, 'closed'); - throw new Error('signin required'); + console.log('signin required'); } diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts index 4479db1081..506c7a9da7 100644 --- a/packages/frontend/src/scripts/time.ts +++ b/packages/frontend/src/scripts/time.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ErrorHandling } from '@/error'; + const dateTimeIntervals = { 'day': 86400000, 'hour': 3600000, @@ -19,7 +21,7 @@ export function dateUTC(time: number[]): Date { : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) : null; - if (!d) throw new Error('wrong number of arguments'); + if (!d) throw ErrorHandling('wrong number of arguments'); return new Date(d); } diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index f5ec4b60b4..cb7ebeaffa 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -12,6 +12,7 @@ import { apiUrl } from '@/config'; import { $i } from '@/account'; import { alert } from '@/os'; import { i18n } from '@/i18n'; +import { ErrorHandling } from '@/error'; type Uploading = { id: string; @@ -34,7 +35,7 @@ export function uploadFile( name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading, ): Promise { - if ($i == null) throw new Error('Not logged in'); + if ($i == null) console.log('Not logged in'); if (folder && typeof folder === 'object') folder = folder.id;