diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2024-07-20 21:33:20 +0900 |
|---|---|---|
| committer | syuilo <4439005+syuilo@users.noreply.github.com> | 2024-07-20 21:33:20 +0900 |
| commit | 337b42bcb179bdfb993888ed94342a0158e8f3cb (patch) | |
| tree | bd40424cf34c72b17effe19e5ce3cf866b3c6241 /packages/backend/src/core | |
| parent | docs(misskey-js): fix broken i-want-you image link in README.md (#14265) (diff) | |
| download | misskey-337b42bcb179bdfb993888ed94342a0158e8f3cb.tar.gz misskey-337b42bcb179bdfb993888ed94342a0158e8f3cb.tar.bz2 misskey-337b42bcb179bdfb993888ed94342a0158e8f3cb.zip | |
revert 5f88d56d96
バグがある(かつすぐに修正できそうにない) & まだレビュー途中で意図せずマージされたため
Diffstat (limited to 'packages/backend/src/core')
21 files changed, 350 insertions, 667 deletions
diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index ca0864f679..69a57b4854 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; +import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; @@ -13,44 +12,30 @@ import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; @Injectable() -export class AccountUpdateService implements OnModuleInit { - private apDeliverManagerService: ApDeliverManagerService; +export class AccountUpdateService { constructor( - private moduleRef: ModuleRef, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userEntityService: UserEntityService, private apRendererService: ApRendererService, + private apDeliverManagerService: ApDeliverManagerService, private relayService: RelayService, ) { } - async onModuleInit() { - this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name); - } - @bindThis - /** - * Deliver account update to followers - * @param userId user id - * @param deliverKey optional. Private key to sign the deliver. - */ - public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKeyWithPem) { + public async publishToFollowers(userId: MiUser['id']) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); - await Promise.allSettled([ - this.apDeliverManagerService.deliverToFollowers(user, content, deliverKey), - this.relayService.deliverToRelays(user, content, deliverKey), - ]); + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); } } } diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 60ddc9cde2..6c5b0f6a36 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull, DataSource } from 'typeorm'; -import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; @@ -38,7 +38,7 @@ export class CreateSystemUserService { // Generate secret const secret = generateNativeUserToken(); - const keyPair = await genRSAAndEd25519KeyPair(); + const keyPair = await genRsaKeyPair(); let account!: MiUser; @@ -64,8 +64,9 @@ export class CreateSystemUserService { }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); await transactionalEntityManager.insert(MiUserKeypair, { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, userId: account.id, - ...keyPair, }); await transactionalEntityManager.insert(MiUserProfile, { diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index dc53c8711d..aa16468ecb 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -15,7 +15,6 @@ import { LoggerService } from '@/core/LoggerService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { REMOTE_SERVER_CACHE_TTL } from '@/const.js'; import type { DOMWindow } from 'jsdom'; type NodeInfo = { @@ -25,7 +24,6 @@ type NodeInfo = { version?: unknown; }; metadata?: { - httpMessageSignaturesImplementationLevel?: unknown, name?: unknown; nodeName?: unknown; nodeDescription?: unknown; @@ -41,7 +39,6 @@ type NodeInfo = { @Injectable() export class FetchInstanceMetadataService { private logger: Logger; - private httpColon = 'https://'; constructor( private httpRequestService: HttpRequestService, @@ -51,7 +48,6 @@ export class FetchInstanceMetadataService { private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); - this.httpColon = process.env.MISSKEY_USE_HTTP?.toLowerCase() === 'true' ? 'http://' : 'https://'; } @bindThis @@ -63,7 +59,7 @@ export class FetchInstanceMetadataService { return await this.redisClient.set( `fetchInstanceMetadata:mutex:v2:${host}`, '1', 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 - 'GET', // 古い値を返す(なかったらnull) + 'GET' // 古い値を返す(なかったらnull) ); } @@ -77,24 +73,23 @@ export class FetchInstanceMetadataService { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> { const host = instance.host; - if (!force) { - // キャッシュ有効チェックはロック取得前に行う - const _instance = await this.federatedInstanceService.fetch(host); - const now = Date.now(); - if (_instance && _instance.infoUpdatedAt != null && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { - this.logger.debug(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`); - return; - } - - // finallyでunlockされてしまうのでtry内でロックチェックをしない - // (returnであってもfinallyは実行される) - if (await this.tryLock(host) === '1') { - // 1が返ってきていたら他にロックされているという意味なので、何もしない - return; - } + // finallyでunlockされてしまうのでtry内でロックチェックをしない + // (returnであってもfinallyは実行される) + if (!force && await this.tryLock(host) === '1') { + // 1が返ってきていたらロックされているという意味なので、何もしない + return; } try { + if (!force) { + const _instance = await this.federatedInstanceService.fetch(host); + const now = Date.now(); + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { + // unlock at the finally caluse + return; + } + } + this.logger.info(`Fetching metadata of ${instance.host} ...`); const [info, dom, manifest] = await Promise.all([ @@ -123,14 +118,6 @@ export class FetchInstanceMetadataService { updates.openRegistrations = info.openRegistrations; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; - if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel && ( - info.metadata.httpMessageSignaturesImplementationLevel === '01' || - info.metadata.httpMessageSignaturesImplementationLevel === '11' - )) { - updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel; - } else { - updates.httpMessageSignaturesImplementationLevel = '00'; - } } if (name) updates.name = name; @@ -142,12 +129,6 @@ export class FetchInstanceMetadataService { await this.federatedInstanceService.update(instance.id, updates); this.logger.succ(`Successfuly updated metadata of ${instance.host}`); - this.logger.debug('Updated metadata:', { - info: !!info, - dom: !!dom, - manifest: !!manifest, - updates, - }); } catch (e) { this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); } finally { @@ -160,7 +141,7 @@ export class FetchInstanceMetadataService { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); try { - const wellknown = await this.httpRequestService.getJson(this.httpColon + instance.host + '/.well-known/nodeinfo') + const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') .catch(err => { if (err.statusCode === 404) { throw new Error('No nodeinfo provided'); @@ -203,7 +184,7 @@ export class FetchInstanceMetadataService { private async fetchDom(instance: MiInstance): Promise<DOMWindow['document']> { this.logger.info(`Fetching HTML of ${instance.host} ...`); - const url = this.httpColon + instance.host; + const url = 'https://' + instance.host; const html = await this.httpRequestService.getHtml(url); @@ -215,7 +196,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchManifest(instance: MiInstance): Promise<Record<string, unknown> | null> { - const url = this.httpColon + instance.host; + const url = 'https://' + instance.host; const manifestUrl = url + '/manifest.json'; @@ -226,7 +207,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise<string | null> { - const url = this.httpColon + instance.host; + const url = 'https://' + instance.host; if (doc) { // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 @@ -253,12 +234,12 @@ export class FetchInstanceMetadataService { @bindThis private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { - const url = this.httpColon + instance.host; + const url = 'https://' + instance.host; return (new URL(manifest.icons[0].src, url)).href; } if (doc) { - const url = this.httpColon + instance.host; + const url = 'https://' + instance.host; // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const links = Array.from(doc.getElementsByTagName('link')).reverse(); diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 312bcfb3b5..87aa70713e 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -249,7 +249,6 @@ export interface InternalEventTypes { unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; }; userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; - userKeypairUpdated: { userId: MiUser['id']; }; } type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>; diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 4249c158d7..7f3cac7c58 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -70,7 +70,7 @@ export class HttpRequestService { localAddress: config.outgoingAddress, }); - const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 16); + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); this.httpAgent = config.proxy ? new HttpProxyAgent({ diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index dd3f2182b4..80827a500b 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -13,6 +13,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import type { DbJobData, DeliverJobData, @@ -32,7 +33,7 @@ import type { UserWebhookDeliverQueue, SystemWebhookDeliverQueue, } from './QueueModule.js'; -import { genRFC3230DigestHeader, type PrivateKeyWithPem, type ParsedSignature } from '@misskey-dev/node-http-message-signatures'; +import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @Injectable() @@ -89,21 +90,21 @@ export class QueueService { } @bindThis - public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKeyWithPem) { + public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { if (content == null) return null; if (to == null) return null; const contentBody = JSON.stringify(content); + const digest = ApRequestCreator.createDigest(contentBody); const data: DeliverJobData = { user: { id: user.id, }, content: contentBody, - digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), + digest, to, isSharedInbox, - privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem }, }; return this.deliverQueue.add(to, data, { @@ -121,13 +122,13 @@ export class QueueService { * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください * @param content IActivity | null * @param inboxes `Map<string, boolean>` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox) - * @param forceMainKey boolean | undefined, force to use main (rsa) key * @returns void */ @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>, privateKey?: PrivateKeyWithPem) { + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) { if (content == null) return null; const contentBody = JSON.stringify(content); + const digest = ApRequestCreator.createDigest(contentBody); const opts = { attempts: this.config.deliverJobMaxAttempts ?? 12, @@ -143,9 +144,9 @@ export class QueueService { data: { user, content: contentBody, + digest, to: d[0], isSharedInbox: d[1], - privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem }, } as DeliverJobData, opts, }))); @@ -154,7 +155,7 @@ export class QueueService { } @bindThis - public inbox(activity: IActivity, signature: ParsedSignature | null) { + public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { const data = { activity: activity, signature, diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index ad01f98902..8dd3d64f5b 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -16,8 +16,6 @@ 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 { UserKeypairService } from './UserKeypairService.js'; -import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -36,7 +34,6 @@ export class RelayService { private queueService: QueueService, private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, - private userKeypairService: UserKeypairService, ) { this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); } @@ -114,7 +111,7 @@ export class RelayService { } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKeyWithPem): Promise<void> { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise<void> { if (activity == null) return; const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ @@ -124,9 +121,11 @@ export class RelayService { const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; - privateKey = privateKey ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id); - const signed = await this.apRendererService.attachLdSignature(copy, privateKey); - this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey); + const signed = await this.apRendererService.attachLdSignature(copy, user); + + for (const relay of relays) { + this.queueService.deliver(user, signed, relay.inbox, false); + } } } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 54c6170062..5522ecd6cc 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; @@ -20,7 +21,6 @@ import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; import { MetaService } from '@/core/MetaService.js'; -import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; @Injectable() export class SignupService { @@ -93,7 +93,22 @@ export class SignupService { } } - const keyPair = await genRSAAndEd25519KeyPair(); + const keyPair = await new Promise<string[]>((res, rej) => + generateKeyPair('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: 'spki', + format: 'pem', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: undefined, + passphrase: undefined, + }, + }, (err, publicKey, privateKey) => + err ? rej(err) : res([publicKey, privateKey]), + )); let account!: MiUser; @@ -116,8 +131,9 @@ export class SignupService { })); await transactionalEntityManager.save(new MiUserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], userId: account.id, - ...keyPair, })); await transactionalEntityManager.save(new MiUserProfile({ diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index aa90f1e209..51ac99179a 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -5,184 +5,41 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { genEd25519KeyPair, importPrivateKey, PrivateKey, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; import type { MiUser } from '@/models/User.js'; import type { UserKeypairsRepository } from '@/models/_.js'; -import { RedisKVCache, MemoryKVCache } from '@/misc/cache.js'; +import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { webcrypto } from 'node:crypto'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { - private keypairEntityCache: RedisKVCache<MiUserKeypair>; - private privateKeyObjectCache: MemoryKVCache<webcrypto.CryptoKey>; + private cache: RedisKVCache<MiUserKeypair>; constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, + @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, - - private globalEventService: GlobalEventService, - private userEntityService: UserEntityService, ) { - this.keypairEntityCache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', { + this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h memoryCacheLifetime: Infinity, fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); - this.privateKeyObjectCache = new MemoryKVCache<webcrypto.CryptoKey>(1000 * 60 * 60 * 1); - - this.redisForSub.on('message', this.onMessage); } @bindThis public async getUserKeypair(userId: MiUser['id']): Promise<MiUserKeypair> { - return await this.keypairEntityCache.fetch(userId); - } - - /** - * Get private key [Only PrivateKeyWithPem for queue data etc.] - * @param userIdOrHint user id or MiUserKeypair - * @param preferType - * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists. - * Otherwise, main keypair will be returned. - * @returns - */ - @bindThis - public async getLocalUserPrivateKeyPem( - userIdOrHint: MiUser['id'] | MiUserKeypair, - preferType?: string, - ): Promise<PrivateKeyWithPem> { - const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; - if ( - preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase()) && - keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null - ) { - return { - keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`, - privateKeyPem: keypair.ed25519PrivateKey, - }; - } - return { - keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, - privateKeyPem: keypair.privateKey, - }; - } - - /** - * Get private key [Only PrivateKey for ap request] - * Using cache due to performance reasons of `crypto.subtle.importKey` - * @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem - * @param preferType - * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists. - * Otherwise, main keypair will be returned. (ignored if userIdOrHint is PrivateKeyWithPem) - * @returns - */ - @bindThis - public async getLocalUserPrivateKey( - userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem, - preferType?: string, - ): Promise<PrivateKey> { - if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) { - // userIdOrHint is PrivateKeyWithPem - return { - keyId: userIdOrHint.keyId, - privateKey: await this.privateKeyObjectCache.fetch(userIdOrHint.keyId, async () => { - return await importPrivateKey(userIdOrHint.privateKeyPem); - }), - }; - } - - const userId = typeof userIdOrHint === 'string' ? userIdOrHint : userIdOrHint.userId; - const getKeypair = () => typeof userIdOrHint === 'string' ? this.getUserKeypair(userId) : userIdOrHint; - - if (preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase())) { - const keyId = `${this.userEntityService.genLocalUserUri(userId)}#ed25519-key`; - const fetched = await this.privateKeyObjectCache.fetchMaybe(keyId, async () => { - const keypair = await getKeypair(); - if (keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) { - return await importPrivateKey(keypair.ed25519PrivateKey); - } - return; - }); - if (fetched) { - return { - keyId, - privateKey: fetched, - }; - } - } - - const keyId = `${this.userEntityService.genLocalUserUri(userId)}#main-key`; - return { - keyId, - privateKey: await this.privateKeyObjectCache.fetch(keyId, async () => { - const keypair = await getKeypair(); - return await importPrivateKey(keypair.privateKey); - }), - }; + return await this.cache.fetch(userId); } @bindThis - public async refresh(userId: MiUser['id']): Promise<void> { - return await this.keypairEntityCache.refresh(userId); - } - - /** - * If DB has ed25519 keypair, refresh cache and return it. - * If not, create, save and return ed25519 keypair. - * @param userId user id - * @returns MiUserKeypair if keypair is created, void if keypair is already exists - */ - @bindThis - public async refreshAndPrepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> { - await this.refresh(userId); - const keypair = await this.keypairEntityCache.fetch(userId); - if (keypair.ed25519PublicKey != null) { - return; - } - - const ed25519 = await genEd25519KeyPair(); - await this.userKeypairsRepository.update({ userId }, { - ed25519PublicKey: ed25519.publicKey, - ed25519PrivateKey: ed25519.privateKey, - }); - this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); - const result = { - ...keypair, - ed25519PublicKey: ed25519.publicKey, - ed25519PrivateKey: ed25519.privateKey, - }; - this.keypairEntityCache.set(userId, result); - return result; - } - - @bindThis - private async onMessage(_: string, data: string): Promise<void> { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message as GlobalEvents['internal']['payload']; - switch (type) { - case 'userKeypairUpdated': { - this.refresh(body.userId); - break; - } - } - } - } - @bindThis public dispose(): void { - this.keypairEntityCache.dispose(); + this.cache.dispose(); } @bindThis diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index fc5a68c72e..d594a223f4 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -3,23 +3,27 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { Not, IsNull } from 'typeorm'; +import type { FollowingsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; +import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import { UserKeypairService } from './UserKeypairService.js'; -import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @Injectable() export class UserSuspendService { constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, + private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, - private userKeypairService: UserKeypairService, - private apDeliverManagerService: ApDeliverManagerService, ) { } @@ -28,12 +32,28 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); if (this.userEntityService.isLocalUser(user)) { + // 知り得る全SharedInboxにDelete配信 const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - const manager = this.apDeliverManagerService.createDeliverManager(user, content); - manager.addAllKnowingSharedInboxRecipe(); - // process deliver時にはキーペアが消去されているはずなので、ここで挿入する - const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main'); - manager.execute({ privateKey }); + + const queue: string[] = []; + + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); + + for (const inbox of inboxes) { + if (inbox != null && !queue.includes(inbox)) queue.push(inbox); + } + + for (const inbox of queue) { + this.queueService.deliver(user, content, inbox, true); + } } } @@ -42,12 +62,28 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); if (this.userEntityService.isLocalUser(user)) { + // 知り得る全SharedInboxにUndo Delete配信 const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); - const manager = this.apDeliverManagerService.createDeliverManager(user, content); - manager.addAllKnowingSharedInboxRecipe(); - // process deliver時にはキーペアが消去されているはずなので、ここで挿入する - const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main'); - manager.execute({ privateKey }); + + const queue: string[] = []; + + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); + + for (const inbox of inboxes) { + if (inbox != null && !queue.includes(inbox)) queue.push(inbox); + } + + for (const inbox of queue) { + this.queueService.deliver(user as any, content, inbox, true); + } } } } diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index aa1144778c..374536a741 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -46,7 +46,7 @@ export class WebfingerService { const m = query.match(mRegex); if (m) { const hostname = m[2]; - const useHttp = process.env.MISSKEY_USE_HTTP && process.env.MISSKEY_USE_HTTP.toLowerCase() === 'true'; + const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true'; return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`; } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 973394683f..f6b70ead44 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { MiUser, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; @@ -13,12 +13,9 @@ import { CacheService } from '@/core/CacheService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import Logger from '@/logger.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; -import { ApLoggerService } from './ApLoggerService.js'; import type { IObject } from './type.js'; -import { UtilityService } from '../UtilityService.js'; export type UriParseResult = { /** wether the URI was generated by us */ @@ -38,8 +35,8 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { - private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey[] | null>; - private logger: Logger; + private publicKeyCache: MemoryKVCache<MiUserPublickey | null>; + private publicKeyByUserIdCache: MemoryKVCache<MiUserPublickey | null>; constructor( @Inject(DI.config) @@ -56,17 +53,9 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, - private apLoggerService: ApLoggerService, - private utilityService: UtilityService, ) { - this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey[] | null>(Infinity); - this.logger = this.apLoggerService.logger.createSubLogger('db-resolver'); - } - - private punyHost(url: string): string { - const urlObj = new URL(url); - const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; - return host; + this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); } @bindThis @@ -127,141 +116,62 @@ export class ApDbResolverService implements OnApplicationShutdown { } } - @bindThis - private async refreshAndFindKey(userId: MiUser['id'], keyId: string): Promise<MiUserPublickey | null> { - this.refreshCacheByUserId(userId); - const keys = await this.getPublicKeyByUserId(userId); - if (keys == null || !Array.isArray(keys) || keys.length === 0) { - this.logger.warn(`No key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); - return null; - } - const exactKey = keys.find(x => x.keyId === keyId); - if (exactKey) return exactKey; - this.logger.warn(`No exact key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); - return null; - } - /** - * AP Actor id => Misskey User and Key - * @param uri AP Actor id - * @param keyId Key id to find. If not specified, main key will be selected. - * @returns - * 1. `null` if the user and key host do not match - * 2. `{ user: null, key: null }` if the user is not found - * 3. `{ user: MiRemoteUser, key: null }` if key is not found - * 4. `{ user: MiRemoteUser, key: MiUserPublickey }` if both are found + * AP KeyId => Misskey User and Key */ @bindThis - public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ + public async getAuthUserFromKeyId(keyId: string): Promise<{ user: MiRemoteUser; - key: MiUserPublickey | null; - } | { - user: null; - key: null; - } | - null> { - if (keyId) { - if (this.punyHost(uri) !== this.punyHost(keyId)) { - /** - * keyIdはURL形式かつkeyIdのホストはuriのホストと一致するはず - * (ApPersonService.validateActorに由来) - * - * ただ、Mastodonはリプライ関連で他人のトゥートをHTTP Signature署名して送ってくることがある - * そのような署名は有効性に疑問があるので無視することにする - * ここではuriとkeyIdのホストが一致しない場合は無視する - * ハッシュをなくしたkeyIdとuriの同一性を比べてみてもいいが、`uri#*-key`というkeyIdを設定するのが - * 決まりごとというわけでもないため幅を持たせることにする - * - * - * The keyId should be in URL format and its host should match the host of the uri - * (derived from ApPersonService.validateActor) - * - * However, Mastodon sometimes sends toots from other users with HTTP Signature signing for reply-related purposes - * Such signatures are of questionable validity, so we choose to ignore them - * Here, we ignore cases where the hosts of uri and keyId do not match - * We could also compare the equality of keyId without the hash and uri, but since setting a keyId like `uri#*-key` - * is not a strict rule, we decide to allow for some flexibility - */ - this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); - return null; - } - } - - const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser; - if (user.isDeleted) return { user: null, key: null }; - - const keys = await this.getPublicKeyByUserId(user.id); - - if (keys == null || !Array.isArray(keys) || keys.length === 0) { - this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${JSON.stringify(keys)}`); - return { user, key: null }; - } - - if (!keyId) { - // Choose the main-like - const mainKey = keys.find(x => { - try { - const url = new URL(x.keyId); - const path = url.pathname.split('/').pop()?.toLowerCase(); - if (url.hash) { - if (url.hash.toLowerCase().includes('main')) { - return true; - } - } else if (path?.includes('main') || path === 'publickey') { - return true; - } - } catch { /* noop */ } - - return false; + key: MiUserPublickey; + } | null> { + const key = await this.publicKeyCache.fetch(keyId, async () => { + const key = await this.userPublickeysRepository.findOneBy({ + keyId, }); - return { user, key: mainKey ?? keys[0] }; - } - const exactKey = keys.find(x => x.keyId === keyId); - if (exactKey) return { user, key: exactKey }; + if (key == null) return null; - /** - * keyIdで見つからない場合、まずはキャッシュを更新して再取得 - * If not found with keyId, update cache and reacquire - */ - const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id); - if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) { - const exactKey = await this.refreshAndFindKey(user.id, keyId); - if (exactKey) return { user, key: exactKey }; - } + return key; + }, key => key != null); - /** - * lastFetchedAtでの更新制限を弱めて再取得 - * Reacquisition with weakened update limit at lastFetchedAt - */ - if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) { - this.logger.info(`Fetching user to find public key uri=${uri} userId=${user.id} keyId=${keyId}`); - const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); - if (renewed == null || renewed.isDeleted) return null; + if (key == null) return null; - return { user, key: await this.refreshAndFindKey(user.id, keyId) }; - } + const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null; + if (user == null) return null; + if (user.isDeleted) return null; - this.logger.warn(`No key found uri=${uri} userId=${user.id} keyId=${keyId}`); - return { user, key: null }; + return { + user, + key, + }; } + /** + * AP Actor id => Misskey User and Key + */ @bindThis - public async getPublicKeyByUserId(userId: MiUser['id']): Promise<MiUserPublickey[] | null> { - return await this.publicKeyByUserIdCache.fetch( - userId, - () => this.userPublickeysRepository.find({ where: { userId } }), + public async getAuthUserFromApId(uri: string): Promise<{ + user: MiRemoteUser; + key: MiUserPublickey | null; + } | null> { + const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; + if (user.isDeleted) return null; + + const key = await this.publicKeyByUserIdCache.fetch( + user.id, + () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null, ); - } - @bindThis - public refreshCacheByUserId(userId: MiUser['id']): void { - this.publicKeyByUserIdCache.delete(userId); + return { + user, + key, + }; } @bindThis public dispose(): void { + this.publicKeyCache.dispose(); this.publicKeyByUserIdCache.dispose(); } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index db3302e6ff..5d07cd8e8f 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -9,14 +9,10 @@ import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; -import { AccountUpdateService } from '@/core/AccountUpdateService.js'; -import type Logger from '@/logger.js'; -import { UserKeypairService } from '../UserKeypairService.js'; -import { ApLoggerService } from './ApLoggerService.js'; -import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; interface IRecipe { type: string; @@ -31,19 +27,12 @@ interface IDirectRecipe extends IRecipe { to: MiRemoteUser; } -interface IAllKnowingSharedInboxRecipe extends IRecipe { - type: 'AllKnowingSharedInbox'; -} - const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => recipe.type === 'Followers'; const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => recipe.type === 'Direct'; -const isAllKnowingSharedInbox = (recipe: IRecipe): recipe is IAllKnowingSharedInboxRecipe => - recipe.type === 'AllKnowingSharedInbox'; - class DeliverManager { private actor: ThinUser; private activity: IActivity | null; @@ -51,18 +40,16 @@ class DeliverManager { /** * Constructor - * @param userKeypairService + * @param userEntityService * @param followingsRepository * @param queueService * @param actor Actor * @param activity Activity to deliver */ constructor( - private userKeypairService: UserKeypairService, + private userEntityService: UserEntityService, private followingsRepository: FollowingsRepository, private queueService: QueueService, - private accountUpdateService: AccountUpdateService, - private logger: Logger, actor: { id: MiUser['id']; host: null; }, activity: IActivity | null, @@ -105,18 +92,6 @@ class DeliverManager { } /** - * Add recipe for all-knowing shared inbox deliver - */ - @bindThis - public addAllKnowingSharedInboxRecipe(): void { - const deliver: IAllKnowingSharedInboxRecipe = { - type: 'AllKnowingSharedInbox', - }; - - this.addRecipe(deliver); - } - - /** * Add recipe * @param recipe Recipe */ @@ -129,44 +104,11 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(opts?: { privateKey?: PrivateKeyWithPem }): Promise<void> { - //#region MIGRATION - if (!opts?.privateKey) { - /** - * ed25519の署名がなければ追加する - */ - const created = await this.userKeypairService.refreshAndPrepareEd25519KeyPair(this.actor.id); - if (created) { - // createdが存在するということは新規作成されたということなので、フォロワーに配信する - this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); - // リモートに配信 - const keyPair = await this.userKeypairService.getLocalUserPrivateKeyPem(created, 'main'); - await this.accountUpdateService.publishToFollowers(this.actor.id, keyPair); - } - } - //#endregion - - //#region collect inboxes by recipes + public async execute(): Promise<void> { // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map<string, boolean>(); - if (this.recipes.some(r => isAllKnowingSharedInbox(r))) { - // all-knowing shared inbox - const followings = await this.followingsRepository.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - for (const following of followings) { - if (following.followeeSharedInbox) inboxes.set(following.followeeSharedInbox, true); - if (following.followerSharedInbox) inboxes.set(following.followerSharedInbox, true); - } - } - // build inbox list // Process follower recipes first to avoid duplication when processing direct recipes later. if (this.recipes.some(r => isFollowers(r))) { @@ -200,49 +142,39 @@ class DeliverManager { inboxes.set(recipe.to.inbox, false); } - //#endregion // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey); - this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`); + await this.queueService.deliverMany(this.actor, this.activity, inboxes); } } @Injectable() export class ApDeliverManagerService { - private logger: Logger; - constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private userKeypairService: UserKeypairService, + private userEntityService: UserEntityService, private queueService: QueueService, - private accountUpdateService: AccountUpdateService, - private apLoggerService: ApLoggerService, ) { - this.logger = this.apLoggerService.logger.createSubLogger('deliver-manager'); } /** * Deliver activity to followers * @param actor * @param activity Activity - * @param forceMainKey Force to use main (rsa) key */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKeyWithPem): Promise<void> { + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise<void> { const manager = new DeliverManager( - this.userKeypairService, + this.userEntityService, this.followingsRepository, this.queueService, - this.accountUpdateService, - this.logger, actor, activity, ); manager.addFollowersRecipe(); - await manager.execute({ privateKey }); + await manager.execute(); } /** @@ -254,11 +186,9 @@ export class ApDeliverManagerService { @bindThis public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise<void> { const manager = new DeliverManager( - this.userKeypairService, + this.userEntityService, this.followingsRepository, this.queueService, - this.accountUpdateService, - this.logger, actor, activity, ); @@ -269,11 +199,10 @@ export class ApDeliverManagerService { @bindThis public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { return new DeliverManager( - this.userKeypairService, + this.userEntityService, this.followingsRepository, this.queueService, - this.accountUpdateService, - this.logger, + actor, activity, ); diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 1bef9fe071..e2164fec1d 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -114,8 +114,15 @@ export class ApInboxService { result = await this.performOneActivity(actor, activity); } - // ついでにリモートユーザーの情報が古かったら更新しておく? - // → No, この関数が呼び出される前に署名検証で更新されているはず + // ついでにリモートユーザーの情報が古かったら更新しておく + if (actor.uri) { + if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { + setImmediate(() => { + this.apPersonService.updatePerson(actor.uri); + }); + } + } + return result; } @bindThis diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 5d7419f934..98e944f347 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -22,6 +22,7 @@ import { UserKeypairService } from '@/core/UserKeypairService.js'; import { MfmService } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -30,7 +31,6 @@ import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.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 type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class ApRendererService { @@ -251,15 +251,15 @@ export class ApRendererService { } @bindThis - public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey { + public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { return { - id: `${this.userEntityService.genLocalUserUri(user.id)}${postfix ?? '/publickey'}`, + id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', owner: this.userEntityService.genLocalUserUri(user.id), - publicKeyPem: createPublicKey(publicKey).export({ + publicKeyPem: createPublicKey(key.publicKey).export({ type: 'spki', format: 'pem', - }) as string, + }), }; } @@ -499,10 +499,7 @@ export class ApRendererService { tag, manuallyApprovesFollowers: user.isLocked, discoverable: user.isExplorable, - publicKey: this.renderKey(user, keypair.publicKey, '#main-key'), - additionalPublicKeys: [ - ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key')] : []), - ], + publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, attachment: attachment.length ? attachment : undefined, }; @@ -625,10 +622,12 @@ export class ApRendererService { } @bindThis - public async attachLdSignature(activity: any, key: PrivateKeyWithPem): Promise<IActivity> { + public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> { + const keypair = await this.userKeypairService.getUserKeypair(user.id); + const jsonLd = this.jsonLdService.use(); jsonLd.debug = false; - activity = await jsonLd.signRsaSignature2017(activity, key.privateKeyPem, key.keyId); + activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); return activity; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 0cae91316b..93ac8ce9a7 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { genRFC3230DigestHeader, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiUser } from '@/models/User.js'; @@ -15,61 +15,122 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import type { PrivateKeyWithPem, PrivateKey } from '@misskey-dev/node-http-message-signatures'; -export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record<string, string> }) { - const u = new URL(args.url); - const request = { - url: u.href, - method: 'POST', - headers: { - 'Date': new Date().toUTCString(), - 'Host': u.host, - 'Content-Type': 'application/activity+json', - ...args.additionalHeaders, - } as Record<string, string>, - }; +type Request = { + url: string; + method: string; + headers: Record<string, string>; +}; - // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする - const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256'); - request.headers['Digest'] = digestHeader; +type Signed = { + request: Request; + signingString: string; + signature: string; + signatureHeader: string; +}; - const result = await signAsDraftToRequest( - request, - args.key, - ['(request-target)', 'date', 'host', 'digest'], - ); +type PrivateKey = { + privateKeyPem: string; + keyId: string; +}; - return { - request, - ...result, - }; -} +export class ApRequestCreator { + static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed { + const u = new URL(args.url); + const digestHeader = args.digest ?? this.createDigest(args.body); + + const request: Request = { + url: u.href, + method: 'POST', + headers: this.#objectAssignWithLcKey({ + 'Date': new Date().toUTCString(), + 'Host': u.host, + 'Content-Type': 'application/activity+json', + 'Digest': digestHeader, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); -export async function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record<string, string> }) { - const u = new URL(args.url); - const request = { - url: u.href, - method: 'GET', - headers: { - 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).host, - ...args.additionalHeaders, - } as Record<string, string>, - }; + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } + + static createDigest(body: string) { + return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; + } + + static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed { + const u = new URL(args.url); + + const request: Request = { + url: u.href, + method: 'GET', + headers: this.#objectAssignWithLcKey({ + 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Date': new Date().toUTCString(), + 'Host': new URL(args.url).host, + }, args.additionalHeaders), + }; + + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + + return { + request, + signingString: result.signingString, + signature: result.signature, + signatureHeader: result.signatureHeader, + }; + } - // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする - const result = await signAsDraftToRequest( - request, - args.key, - ['(request-target)', 'date', 'host', 'accept'], - ); + static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed { + const signingString = this.#genSigningString(request, includeHeaders); + const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); + const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; + + request.headers = this.#objectAssignWithLcKey(request.headers, { + Signature: signatureHeader, + }); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! + delete request.headers['host']; + + return { + request, + signingString, + signature, + signatureHeader, + }; + } + + static #genSigningString(request: Request, includeHeaders: string[]): string { + request.headers = this.#lcObjectKey(request.headers); + + const results: string[] = []; + + for (const key of includeHeaders.map(x => x.toLowerCase())) { + if (key === '(request-target)') { + results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); + } else { + results.push(`${key}: ${request.headers[key]}`); + } + } + + return results.join('\n'); + } - return { - request, - ...result, - }; + static #lcObjectKey(src: Record<string, string>): Record<string, string> { + const dst: Record<string, string> = {}; + for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; + return dst; + } + + static #objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>): Record<string, string> { + return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b)); + } } @Injectable() @@ -89,28 +150,21 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise<void> { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> { const body = typeof object === 'string' ? object : JSON.stringify(object); - const keyFetched = await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level); - const req = await createSignedPost({ - level, - key: keyFetched, + + const keypair = await this.userKeypairService.getUserKeypair(user.id); + + const req = ApRequestCreator.createSignedPost({ + key: { + privateKeyPem: keypair.privateKey, + keyId: `${this.config.url}/users/${user.id}#main-key`, + }, url, body, + digest, additionalHeaders: { - 'User-Agent': this.config.userAgent, }, - digest, - }); - - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete req.request.headers['Host']; - - this.logger.debug('create signed post', { - version: 'draft', - level, - url, - keyId: keyFetched.keyId, }); await this.httpRequestService.send(url, { @@ -126,27 +180,19 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> { - const key = await this.userKeypairService.getLocalUserPrivateKey(user.id, level); - const req = await createSignedGet({ - level, - key, + public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> { + const keypair = await this.userKeypairService.getUserKeypair(user.id); + + const req = ApRequestCreator.createSignedGet({ + key: { + privateKeyPem: keypair.privateKey, + keyId: `${this.config.url}/users/${user.id}#main-key`, + }, url, additionalHeaders: { - 'User-Agent': this.config.userAgent, }, }); - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete req.request.headers['Host']; - - this.logger.debug('create signed get', { - version: 'draft', - level, - url, - keyId: key.keyId, - }); - const res = await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 727ff6f956..bb3c40f093 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -16,7 +16,6 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; -import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; @@ -42,7 +41,6 @@ export class Resolver { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, - private federatedInstanceService: FederatedInstanceService, private loggerService: LoggerService, private recursionLimit = 100, ) { @@ -105,10 +103,8 @@ export class Resolver { this.user = await this.instanceActorService.getInstanceActor(); } - const server = await this.federatedInstanceService.fetch(host); - const object = (this.user - ? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject + ? await this.apRequestService.signedGet(value, this.user) as IObject : await this.httpRequestService.getActivityJson(value)) as IObject; if ( @@ -204,7 +200,6 @@ export class ApResolverService { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, - private federatedInstanceService: FederatedInstanceService, private loggerService: LoggerService, ) { } @@ -225,7 +220,6 @@ export class ApResolverService { this.httpRequestService, this.apRendererService, this.apDbResolverService, - this.federatedInstanceService, this.loggerService, ); } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index fc4e3e3bef..feb8c42c56 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -134,7 +134,6 @@ const security_v1 = { 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, 'privateKeyPem': 'sec:privateKeyPem', 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, - 'additionalPublicKeys': { '@id': 'sec:publicKey', '@type': '@id' }, 'publicKeyBase58': 'sec:publicKeyBase58', 'publicKeyPem': 'sec:publicKeyPem', 'publicKeyWif': 'sec:publicKeyWif', diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index c41fc713d5..457205e023 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -3,10 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { verify } from 'crypto'; import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DataSource, In, Not } from 'typeorm'; +import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; @@ -40,7 +39,6 @@ import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; -import { REMOTE_USER_CACHE_TTL, REMOTE_USER_MOVE_COOLDOWN } from '@/const.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -50,7 +48,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; -import type { IActor, IKey, IObject } from '../type.js'; +import type { IActor, IObject } from '../type.js'; const nameLength = 128; const summaryLength = 2048; @@ -187,38 +185,13 @@ export class ApPersonService implements OnModuleInit { } if (x.publicKey) { - const publicKeys = Array.isArray(x.publicKey) ? x.publicKey : [x.publicKey]; - - for (const publicKey of publicKeys) { - if (typeof publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); - } - - const publicKeyIdHost = this.punyHost(publicKey.id); - if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); - } + if (typeof x.publicKey.id !== 'string') { + throw new Error('invalid Actor: publicKey.id is not a string'); } - } - if (x.additionalPublicKeys) { - if (!x.publicKey) { - throw new Error('invalid Actor: additionalPublicKeys is set but publicKey is not'); - } - - if (!Array.isArray(x.additionalPublicKeys)) { - throw new Error('invalid Actor: additionalPublicKeys is not an array'); - } - - for (const key of x.additionalPublicKeys) { - if (typeof key.id !== 'string') { - throw new Error('invalid Actor: additionalPublicKeys.id is not a string'); - } - - const keyIdHost = this.punyHost(key.id); - if (keyIdHost !== expectHost) { - throw new Error('invalid Actor: additionalPublicKeys.id has different host'); - } + const publicKeyIdHost = this.punyHost(x.publicKey.id); + if (publicKeyIdHost !== expectHost) { + throw new Error('invalid Actor: publicKey.id has different host'); } } @@ -255,33 +228,6 @@ export class ApPersonService implements OnModuleInit { return null; } - /** - * uriからUser(Person)をフェッチします。 - * - * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 - * また、TTLが0でない場合、TTLを過ぎていた場合はupdatePersonを実行します。 - */ - @bindThis - async fetchPersonWithRenewal(uri: string, TTL = REMOTE_USER_CACHE_TTL): Promise<MiLocalUser | MiRemoteUser | null> { - const exist = await this.fetchPerson(uri); - if (exist == null) return null; - - if (this.userEntityService.isRemoteUser(exist)) { - if (TTL === 0 || exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > TTL) { - this.logger.debug('fetchPersonWithRenewal: renew', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); - try { - await this.updatePerson(exist.uri); - return await this.fetchPerson(uri); - } catch (err) { - this.logger.error('error occurred while renewing user', { err }); - } - } - this.logger.debug('fetchPersonWithRenewal: use cache', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); - } - - return exist; - } - private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise<Partial<Pick<MiRemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>>> { if (user == null) throw new Error('failed to create user: user is null'); @@ -417,15 +363,11 @@ export class ApPersonService implements OnModuleInit { })); if (person.publicKey) { - const publicKeys = new Map<string, IKey>(); - (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); - (Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key)); - - await transactionalEntityManager.save(Array.from(publicKeys.values(), key => new MiUserPublickey({ - keyId: key.id, - userId: user!.id, - keyPem: key.publicKeyPem, - }))); + await transactionalEntityManager.save(new MiUserPublickey({ + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem, + })); } }); } catch (e) { @@ -571,29 +513,11 @@ export class ApPersonService implements OnModuleInit { // Update user await this.usersRepository.update(exist.id, updates); - try { - // Deleteアクティビティ受信時にもここが走ってsaveがuserforeign key制約エラーを吐くことがある - // とりあえずtry-catchで囲っておく - const publicKeys = new Map<string, IKey>(); - if (person.publicKey) { - (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); - (Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key)); - - await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ - keyId: key.id, - userId: exist.id, - keyPem: key.publicKeyPem, - }))); - } - - this.userPublickeysRepository.delete({ - keyId: Not(In(Array.from(publicKeys.keys()))), - userId: exist.id, - }).catch(err => { - this.logger.error('something happened while deleting remote user public keys:', { userId: exist.id, err }); + if (person.publicKey) { + await this.userPublickeysRepository.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem, }); - } catch (err) { - this.logger.error('something happened while updating remote user public keys:', { userId: exist.id, err }); } let _description: string | null = null; @@ -635,7 +559,7 @@ export class ApPersonService implements OnModuleInit { exist.movedAt == null || // 以前のmovingから14日以上経過した場合のみ移行処理を許可 // (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく) - exist.movedAt.getTime() + REMOTE_USER_MOVE_COOLDOWN < updated.movedAt.getTime() + exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime() )) { this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`); return this.processRemoteMove(updated, movePreventUris) @@ -658,9 +582,9 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver, withRenewal = false): Promise<MiLocalUser | MiRemoteUser> { + public async resolvePerson(uri: string, resolver?: Resolver): Promise<MiLocalUser | MiRemoteUser> { //#region このサーバーに既に登録されていたらそれを返す - const exist = withRenewal ? await this.fetchPersonWithRenewal(uri) : await this.fetchPerson(uri); + const exist = await this.fetchPerson(uri); if (exist) return exist; //#endregion diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 1d55971660..5b6c6c8ca6 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -55,7 +55,7 @@ export function getOneApId(value: ApObject): string { export function getApId(value: string | IObject): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot determine id'); + throw new Error('cannot detemine id'); } /** @@ -169,8 +169,10 @@ export interface IActor extends IObject { discoverable?: boolean; inbox: string; sharedInbox?: string; // 後方互換性のため - publicKey?: IKey | IKey[]; - additionalPublicKeys?: IKey[]; + publicKey?: { + id: string; + publicKeyPem: string; + }; followers?: string | ICollection | IOrderedCollection; following?: string | ICollection | IOrderedCollection; featured?: string | IOrderedCollection; @@ -234,9 +236,8 @@ export const isEmoji = (object: IObject): object is IApEmoji => export interface IKey extends IObject { type: 'Key'; - id: string; owner: string; - publicKeyPem: string; + publicKeyPem: string | Buffer; } export interface IApDocument extends IObject { diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index fd0f55c6ab..9117b13914 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -56,7 +56,6 @@ export class InstanceEntityService { infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, moderationNote: iAmModerator ? instance.moderationNote : null, - httpMessageSignaturesImplementationLevel: instance.httpMessageSignaturesImplementationLevel, }; } |