From 03185aae2e4af93be8011ad9f8f97eeaf4c7ddf3 Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Mon, 19 Jul 2021 15:41:23 -0700
Subject: [PATCH 1/5] Add migration for allowedHosts, secureMode, privateMode

---
 .../1626733991004-allowlist-secure-mode.js      | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)
 create mode 100644 packages/backend/migration/1626733991004-allowlist-secure-mode.js

diff --git a/packages/backend/migration/1626733991004-allowlist-secure-mode.js b/packages/backend/migration/1626733991004-allowlist-secure-mode.js
new file mode 100644
index 000000000..aa3fcf875
--- /dev/null
+++ b/packages/backend/migration/1626733991004-allowlist-secure-mode.js
@@ -0,0 +1,17 @@
+
+
+export class allowlistSecureMode1626733991004  {
+	name = 'allowlistSecureMode1626733991004';
+	async up(queryRunner) {
+		await queryRunner.query(`ALTER TABLE "meta" ADD "allowedHosts" character varying(256) [] default '{}'`);
+		await queryRunner.query(`ALTER TABLE "meta" ADD "secureMode" bool default false`);
+		await queryRunner.query(`ALTER TABLE "meta" ADD "privateMode" bool default false`);
+	}
+
+	async down(queryRunner) {
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedHosts"`);
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "secureMode"`);
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privateMode"`);
+	}
+}
+

From 8f6605eb632a7b37b42116b44f3e098d1b804c59 Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Tue, 20 Jul 2021 09:45:41 -0700
Subject: [PATCH 2/5] Add Secure Mode and Private Mode - Add instance actor -
 Add private mode, which uses an allowlist - Add Secure Mode, restricts access
 to blocked instances

---
 packages/backend/src/models/entities/meta.ts  |  15 +++
 .../backend/src/queue/processors/deliver.ts   |   4 +
 .../backend/src/queue/processors/inbox.ts     |   5 +
 .../src/remote/activitypub/check-fetch.ts     |  73 ++++++++++
 .../src/remote/activitypub/resolver.ts        |   4 +
 packages/backend/src/server/activitypub.ts    | 127 ++++++++++++++++--
 .../src/server/activitypub/featured.ts        |  16 ++-
 .../src/server/activitypub/followers.ts       |  15 ++-
 .../src/server/activitypub/following.ts       |  15 ++-
 .../backend/src/server/activitypub/outbox.ts  |  16 ++-
 .../backend/src/server/api/endpoints/meta.ts  |  12 +-
 11 files changed, 289 insertions(+), 13 deletions(-)
 create mode 100644 packages/backend/src/remote/activitypub/check-fetch.ts

diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index d33ff2519..42c763d27 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -77,6 +77,21 @@ export class Meta {
 	})
 	public blockedHosts: string[];
 
+	@Column('boolean', {
+		default: false
+	})
+	public secureMode: boolean;
+
+	@Column('boolean', {
+		default: false
+	})
+	public privateMode: boolean;
+
+	@Column('varchar', {
+		length: 256, array: true, default: '{}'
+	})
+	public allowedHosts: string[];
+
 	@Column('varchar', {
 		length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}',
 	})
diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts
index 291c05766..01ba4381a 100644
--- a/packages/backend/src/queue/processors/deliver.ts
+++ b/packages/backend/src/queue/processors/deliver.ts
@@ -28,6 +28,10 @@ export default async (job: Bull.Job<DeliverJobData>) => {
 		return 'skip (blocked)';
 	}
 
+	if (meta.privateMode && !meta.allowedHosts.includes(toPuny(host))) {
+		return 'skip (not allowed)';
+	}
+
 	// isSuspendedなら中断
 	let suspendedHosts = suspendedHostsCache.get(null);
 	if (suspendedHosts == null) {
diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts
index 198dde605..422632059 100644
--- a/packages/backend/src/queue/processors/inbox.ts
+++ b/packages/backend/src/queue/processors/inbox.ts
@@ -39,6 +39,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
 		return `Blocked request: ${host}`;
 	}
 
+	// 非公開モードなら許可なインスタンスのみ
+	if (meta.privateMode && !meta.allowedHosts.includes(host)) {
+		return `Blocked request: ${host}`;
+	}
+
 	const keyIdLower = signature.keyId.toLowerCase();
 	if (keyIdLower.startsWith('acct:')) {
 		return `Old keyId is no longer supported. ${keyIdLower}`;
diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts
new file mode 100644
index 000000000..8a53396b6
--- /dev/null
+++ b/packages/backend/src/remote/activitypub/check-fetch.ts
@@ -0,0 +1,73 @@
+import config from '@/config/index.js';
+import { IncomingMessage } from 'http';
+import { fetchMeta } from '@/misc/fetch-meta.js';
+import httpSignature from '@peertube/http-signature';
+import { URL } from 'url';
+import { toPuny } from '@/misc/convert-host.js';
+import DbResolver from '@/remote/activitypub/db-resolver.js';
+import { getApId } from '@/remote/activitypub/type.js';
+
+
+export default async function checkFetch(req: IncomingMessage): Promise<number> {
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		let signature;
+
+		try {
+			signature = httpSignature.parseRequest(req, { 'headers': [] });
+		} catch (e) {
+			return 401;
+		}
+
+		const keyId = new URL(signature.keyId);
+		const host = toPuny(keyId.hostname);
+
+		if (meta.blockedHosts.includes(host)) {
+			return 403;
+		}
+
+		if (meta.privateMode && host !== config.host && !meta.allowedHosts.includes(host)) {
+			return 403;
+		}
+
+		const keyIdLower = signature.keyId.toLowerCase();
+		if (keyIdLower.startsWith('acct:')) {
+			// Old keyId is no longer supported.
+			return 401;
+		}
+
+		const dbResolver = new DbResolver();
+
+		// HTTP-Signature keyIdを元にDBから取得
+		let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
+
+		// keyIdでわからなければ、resolveしてみる
+		if (authUser == null) {
+			try {
+				keyId.hash = '';
+				authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
+			} catch (e) {
+				// できなければ駄目
+				return 403;
+			}
+		}
+
+		// publicKey がなくても終了
+		if (authUser?.key == null) {
+			return 403;
+		}
+
+		// もう一回チェック
+		if (authUser.user.host !== host) {
+			return 403;
+		}
+
+		// HTTP-Signatureの検証
+		const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
+
+		if (!httpSignatureValidated) {
+			return 403;
+		}
+	}
+	return 200;
+}
diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts
index 2f9af43c0..e89a7b21a 100644
--- a/packages/backend/src/remote/activitypub/resolver.ts
+++ b/packages/backend/src/remote/activitypub/resolver.ts
@@ -72,6 +72,10 @@ export default class Resolver {
 			throw new Error('Instance is blocked');
 		}
 
+		if (meta.privateMode && config.host !== host && !meta.allowedHosts.includes(host)) {
+			throw new Error('Instance is not allowed');
+		}
+
 		if (config.signToActivityPubGet && !this.user) {
 			this.user = await getInstanceActor();
 		}
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index cd5f917c4..250a39bf0 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -12,12 +12,15 @@ import Followers from './activitypub/followers.js';
 import Following from './activitypub/following.js';
 import Featured from './activitypub/featured.js';
 import { inbox as processInbox } from '@/queue/index.js';
-import { isSelfHost } from '@/misc/convert-host.js';
+import { isSelfHost, toPuny } from '@/misc/convert-host.js';
 import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
 import { ILocalUser, User } from '@/models/entities/user.js';
 import { In, IsNull, Not } from 'typeorm';
 import { renderLike } from '@/remote/activitypub/renderer/like.js';
 import { getUserKeypair } from '@/misc/keypair-store.js';
+import checkFetch from '@/remote/activitypub/check-fetch.js';
+import { getInstanceActor } from '@/services/instance-actor.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 import renderFollow from '@/remote/activitypub/renderer/follow.js';
 
 // Init router
@@ -66,6 +69,12 @@ router.post('/users/:user/inbox', json(), inbox);
 router.get('/notes/:note', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const note = await Notes.findOneBy({
 		id: ctx.params.note,
 		visibility: In(['public' as const, 'home' as const]),
@@ -88,12 +97,24 @@ router.get('/notes/:note', async (ctx, next) => {
 	}
 
 	ctx.body = renderActivity(await renderNote(note, false));
-	ctx.set('Cache-Control', 'public, max-age=180');
+
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 });
 
 // note activity
 router.get('/notes/:note/activity', async ctx => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const note = await Notes.findOneBy({
 		id: ctx.params.note,
 		userHost: IsNull(),
@@ -107,7 +128,12 @@ router.get('/notes/:note/activity', async ctx => {
 	}
 
 	ctx.body = renderActivity(await packActivity(note));
-	ctx.set('Cache-Control', 'public, max-age=180');
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 });
 
@@ -125,6 +151,20 @@ router.get('/users/:user/collections/featured', Featured);
 
 // publickey
 router.get('/users/:user/publickey', async ctx => {
+	const instanceActor = await getInstanceActor();
+	if (ctx.params.user === instanceActor.id) {
+		ctx.body = renderActivity(renderKey(instanceActor, await getUserKeypair(instanceActor.id)));
+		ctx.set('Cache-Control', 'public, max-age=180');
+		setResponseType(ctx);
+		return;
+	}
+
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const user = await Users.findOneBy({
@@ -141,7 +181,12 @@ router.get('/users/:user/publickey', async ctx => {
 
 	if (Users.isLocalUser(user)) {
 		ctx.body = renderActivity(renderKey(user, keypair));
-		ctx.set('Cache-Control', 'public, max-age=180');
+		const meta = await fetchMeta();
+		if (meta.secureMode || meta.privateMode) {
+			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+		} else {
+			ctx.set('Cache-Control', 'public, max-age=180');
+		}
 		setResponseType(ctx);
 	} else {
 		ctx.status = 400;
@@ -156,13 +201,30 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
 	}
 
 	ctx.body = renderActivity(await renderPerson(user as ILocalUser));
-	ctx.set('Cache-Control', 'public, max-age=180');
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 }
 
 router.get('/users/:user', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
+	const instanceActor = await getInstanceActor();
+	if (ctx.params.user === instanceActor.id) {
+		await userInfo(ctx, instanceActor);
+		return;
+	}
+
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const user = await Users.findOneBy({
@@ -177,6 +239,18 @@ router.get('/users/:user', async (ctx, next) => {
 router.get('/@:user', async (ctx, next) => {
 	if (!isActivityPubReq(ctx)) return await next();
 
+	if (ctx.params.user === 'instance.actor') {
+		const instanceActor = await getInstanceActor();
+		await userInfo(ctx, instanceActor);
+		return;
+	}
+
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const user = await Users.findOneBy({
 		usernameLower: ctx.params.user.toLowerCase(),
 		host: IsNull(),
@@ -185,10 +259,21 @@ router.get('/@:user', async (ctx, next) => {
 
 	await userInfo(ctx, user);
 });
+
+router.get('/actor', async (ctx, next) => {
+	const instanceActor = await getInstanceActor();
+	await userInfo(ctx, instanceActor);
+});
 //#endregion
 
 // emoji
 router.get('/emojis/:emoji', async ctx => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const emoji = await Emojis.findOneBy({
 		host: IsNull(),
 		name: ctx.params.emoji,
@@ -200,12 +285,23 @@ router.get('/emojis/:emoji', async ctx => {
 	}
 
 	ctx.body = renderActivity(await renderEmoji(emoji));
-	ctx.set('Cache-Control', 'public, max-age=180');
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 });
 
 // like
 router.get('/likes/:like', async ctx => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const reaction = await NoteReactions.findOneBy({ id: ctx.params.like });
 
 	if (reaction == null) {
@@ -221,12 +317,22 @@ router.get('/likes/:like', async ctx => {
 	}
 
 	ctx.body = renderActivity(await renderLike(reaction, note));
-	ctx.set('Cache-Control', 'public, max-age=180');
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 });
 
 // follow
 router.get('/follows/:follower/:followee', async ctx => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
 	// This may be used before the follow is completed, so we do not
 	// check if the following exists.
 
@@ -247,7 +353,12 @@ router.get('/follows/:follower/:followee', async ctx => {
 	}
 
 	ctx.body = renderActivity(renderFollow(follower, followee));
-	ctx.set('Cache-Control', 'public, max-age=180');
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 });
 
diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts
index c03fd1049..87e4f8320 100644
--- a/packages/backend/src/server/activitypub/featured.ts
+++ b/packages/backend/src/server/activitypub/featured.ts
@@ -6,8 +6,16 @@ import { setResponseType } from '../activitypub.js';
 import renderNote from '@/remote/activitypub/renderer/note.js';
 import { Users, Notes, UserNotePinings } from '@/models/index.js';
 import { IsNull } from 'typeorm';
+import checkFetch from '@/remote/activitypub/check-fetch.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 
 export default async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const user = await Users.findOneBy({
@@ -36,6 +44,12 @@ export default async (ctx: Router.RouterContext) => {
 	);
 
 	ctx.body = renderActivity(rendered);
-	ctx.set('Cache-Control', 'public, max-age=180');
+
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 	setResponseType(ctx);
 };
diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts
index beb48713a..833f0e77f 100644
--- a/packages/backend/src/server/activitypub/followers.ts
+++ b/packages/backend/src/server/activitypub/followers.ts
@@ -9,8 +9,16 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
 import { Users, Followings, UserProfiles } from '@/models/index.js';
 import { Following } from '@/models/entities/following.js';
 import { setResponseType } from '../activitypub.js';
+import checkFetch from '@/remote/activitypub/check-fetch.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 
 export default async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const cursor = ctx.request.query.cursor;
@@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
 		// index page
 		const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
 		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
 		setResponseType(ctx);
 	}
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 };
diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts
index 3a25a6316..f843d79f0 100644
--- a/packages/backend/src/server/activitypub/following.ts
+++ b/packages/backend/src/server/activitypub/following.ts
@@ -9,8 +9,16 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
 import { Users, Followings, UserProfiles } from '@/models/index.js';
 import { Following } from '@/models/entities/following.js';
 import { setResponseType } from '../activitypub.js';
+import checkFetch from '@/remote/activitypub/check-fetch.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 
 export default async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const cursor = ctx.request.query.cursor;
@@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
 		// index page
 		const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
 		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
 		setResponseType(ctx);
 	}
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 };
diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts
index 7a2586998..4a6b5caff 100644
--- a/packages/backend/src/server/activitypub/outbox.ts
+++ b/packages/backend/src/server/activitypub/outbox.ts
@@ -13,8 +13,16 @@ import { Users, Notes } from '@/models/index.js';
 import { Note } from '@/models/entities/note.js';
 import { makePaginationQuery } from '../api/common/make-pagination-query.js';
 import { setResponseType } from '../activitypub.js';
+import checkFetch from '@/remote/activitypub/check-fetch.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 
 export default async (ctx: Router.RouterContext) => {
+	const verify = await checkFetch(ctx.req);
+	if (verify != 200) {
+		ctx.status = verify;
+		return;
+	}
+
 	const userId = ctx.params.user;
 
 	const sinceId = ctx.request.query.since_id;
@@ -89,9 +97,15 @@ export default async (ctx: Router.RouterContext) => {
 			`${partOf}?page=true&since_id=000000000000000000000000`,
 		);
 		ctx.body = renderActivity(rendered);
-		ctx.set('Cache-Control', 'public, max-age=180');
+
 		setResponseType(ctx);
 	}
+	const meta = await fetchMeta();
+	if (meta.secureMode || meta.privateMode) {
+		ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	} else {
+		ctx.set('Cache-Control', 'public, max-age=180');
+	}
 };
 
 /**
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 5b624842c..ca6b471d5 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -291,6 +291,16 @@ export const meta = {
 					},
 				},
 			},
+			secureMode: {
+				type: 'boolean',
+				optional: true, nullable: false,
+				default: false,
+			},
+			privateMode: {
+				type: 'boolean',
+				optional: true, nullable: false,
+				default: false,
+			},
 		},
 	},
 } as const;
@@ -326,7 +336,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			expiresAt: MoreThan(new Date()),
 		},
 	});
-
+	// TODO: add secure mode, etc
 	const response: any = {
 		maintainerName: instance.maintainerName,
 		maintainerEmail: instance.maintainerEmail,

From 7131ca8897f46b3953886073053ece879b2f9524 Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Tue, 20 Jul 2021 11:51:59 -0700
Subject: [PATCH 3/5] In private mode, block access to many public APIs

---
 packages/backend/src/server/api/call.ts       | 13 ++++++++++++
 packages/backend/src/server/api/endpoints.ts  |  6 ++++++
 .../src/server/api/endpoints/announcements.ts |  1 +
 .../server/api/endpoints/channels/featured.ts |  1 +
 .../src/server/api/endpoints/channels/show.ts |  1 +
 .../server/api/endpoints/channels/timeline.ts |  1 +
 .../api/endpoints/charts/active-users.ts      |  1 +
 .../server/api/endpoints/charts/ap-request.ts |  1 +
 .../src/server/api/endpoints/charts/drive.ts  |  1 +
 .../server/api/endpoints/charts/federation.ts |  1 +
 .../server/api/endpoints/charts/hashtag.ts    |  1 +
 .../server/api/endpoints/charts/instance.ts   |  1 +
 .../src/server/api/endpoints/charts/notes.ts  |  1 +
 .../server/api/endpoints/charts/user/drive.ts |  1 +
 .../api/endpoints/charts/user/following.ts    |  1 +
 .../server/api/endpoints/charts/user/notes.ts |  1 +
 .../api/endpoints/charts/user/reactions.ts    |  1 +
 .../src/server/api/endpoints/charts/users.ts  |  1 +
 .../src/server/api/endpoints/clips/notes.ts   |  1 +
 .../src/server/api/endpoints/clips/show.ts    |  1 +
 .../api/endpoints/federation/followers.ts     |  1 +
 .../api/endpoints/federation/following.ts     |  1 +
 .../api/endpoints/federation/instances.ts     |  1 +
 .../api/endpoints/federation/show-instance.ts |  1 +
 .../server/api/endpoints/federation/users.ts  |  1 +
 .../server/api/endpoints/gallery/featured.ts  |  1 +
 .../server/api/endpoints/gallery/popular.ts   |  1 +
 .../src/server/api/endpoints/gallery/posts.ts |  1 +
 .../api/endpoints/gallery/posts/show.ts       |  1 +
 .../api/endpoints/get-online-users-count.ts   |  1 +
 .../src/server/api/endpoints/hashtags/list.ts |  1 +
 .../server/api/endpoints/hashtags/search.ts   |  1 +
 .../src/server/api/endpoints/hashtags/show.ts |  1 +
 .../server/api/endpoints/hashtags/trend.ts    |  1 +
 .../server/api/endpoints/hashtags/users.ts    |  1 +
 .../backend/src/server/api/endpoints/meta.ts  | 20 ++++++++++++-------
 .../backend/src/server/api/endpoints/notes.ts |  1 +
 .../server/api/endpoints/notes/children.ts    |  3 ++-
 .../src/server/api/endpoints/notes/clips.ts   |  1 +
 .../api/endpoints/notes/conversation.ts       |  1 +
 .../server/api/endpoints/notes/featured.ts    |  1 +
 .../api/endpoints/notes/global-timeline.ts    |  1 +
 .../api/endpoints/notes/local-timeline.ts     |  1 +
 .../server/api/endpoints/notes/reactions.ts   |  1 +
 .../src/server/api/endpoints/notes/renotes.ts |  1 +
 .../src/server/api/endpoints/notes/replies.ts |  1 +
 .../api/endpoints/notes/search-by-tag.ts      |  1 +
 .../src/server/api/endpoints/notes/search.ts  |  1 +
 .../src/server/api/endpoints/notes/show.ts    |  1 +
 .../server/api/endpoints/notes/translate.ts   |  1 +
 .../server/api/endpoints/pages/featured.ts    |  1 +
 .../src/server/api/endpoints/pages/show.ts    |  1 +
 .../src/server/api/endpoints/pinned-users.ts  |  1 +
 .../src/server/api/endpoints/server-info.ts   |  1 +
 .../backend/src/server/api/endpoints/stats.ts |  1 +
 .../backend/src/server/api/endpoints/users.ts |  1 +
 .../src/server/api/endpoints/users/clips.ts   |  1 +
 .../server/api/endpoints/users/followers.ts   |  1 +
 .../server/api/endpoints/users/following.ts   |  1 +
 .../api/endpoints/users/gallery/posts.ts      |  1 +
 .../users/get-frequently-replied-users.ts     |  1 +
 .../src/server/api/endpoints/users/notes.ts   |  1 +
 .../src/server/api/endpoints/users/pages.ts   |  1 +
 .../server/api/endpoints/users/reactions.ts   |  1 +
 .../users/search-by-username-and-host.ts      |  1 +
 .../src/server/api/endpoints/users/search.ts  |  1 +
 .../src/server/api/endpoints/users/show.ts    |  1 +
 .../src/server/api/endpoints/users/stats.ts   |  1 +
 68 files changed, 98 insertions(+), 8 deletions(-)

diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts
index aa130459a..9458d15fe 100644
--- a/packages/backend/src/server/api/call.ts
+++ b/packages/backend/src/server/api/call.ts
@@ -7,6 +7,8 @@ import { limiter } from './limiter.js';
 import endpoints, { IEndpointMeta } from './endpoints.js';
 import { ApiError } from './error.js';
 import { apiLogger } from './logger.js';
+import { AccessToken } from '@/models/entities/access-token.js';
+import { fetchMeta } from '@/misc/fetch-meta.js';
 
 const accessDenied = {
 	message: 'Access denied.',
@@ -93,6 +95,17 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
 		});
 	}
 
+	// private mode
+	const meta = await fetchMeta();
+	if (meta.privateMode && ep.meta.requireCredentialPrivateMode && user == null) {
+		throw new ApiError({
+			message: 'Credential required.',
+			code: 'CREDENTIAL_REQUIRED',
+			id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+			httpStatusCode: 401
+		});
+	}
+
 	// Cast non JSON input
 	if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
 		for (const k of Object.keys(ep.params.properties)) {
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 4644f34d9..d7fcc32d3 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -706,6 +706,12 @@ export interface IEndpointMeta {
 	 */
 	readonly secure?: boolean;
 
+	/**
+	 * プライベートモードでなら、このエンドポイントにリクエストするときにユーザー情報が必要か否か
+	 * 省略した場合は false として解釈されます
+	 */
+	readonly requireCredentialPrivateMode?: boolean;
+
 	/**
 	 * エンドポイントの種類
 	 * パーミッションの実現に利用されます。
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
index 23cb93c9a..189de042b 100644
--- a/packages/backend/src/server/api/endpoints/announcements.ts
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['meta'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts
index 73980c0fa..13ad6ca7d 100644
--- a/packages/backend/src/server/api/endpoints/channels/featured.ts
+++ b/packages/backend/src/server/api/endpoints/channels/featured.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['channels'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts
index 87665a986..1c8461af4 100644
--- a/packages/backend/src/server/api/endpoints/channels/show.ts
+++ b/packages/backend/src/server/api/endpoints/channels/show.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['channels'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index deaa29901..18ba6b2e3 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['notes', 'channels'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts
index ea2379429..216676020 100644
--- a/packages/backend/src/server/api/endpoints/charts/active-users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts', 'users'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(activeUsersChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts
index 06dee250e..a8f6e4564 100644
--- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts
+++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(apRequestChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts
index dd2c2d683..14f82e39d 100644
--- a/packages/backend/src/server/api/endpoints/charts/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/drive.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts', 'drive'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(driveChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts
index 8c35b3c46..141e005ee 100644
--- a/packages/backend/src/server/api/endpoints/charts/federation.ts
+++ b/packages/backend/src/server/api/endpoints/charts/federation.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(federationChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
index 77e24a62c..d34153bc1 100644
--- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts
+++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts', 'hashtags'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(hashtagChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts
index 817d51ad0..3d9619d24 100644
--- a/packages/backend/src/server/api/endpoints/charts/instance.ts
+++ b/packages/backend/src/server/api/endpoints/charts/instance.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(instanceChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts
index 951adf540..42befed27 100644
--- a/packages/backend/src/server/api/endpoints/charts/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/notes.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts', 'notes'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(notesChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
index f165b4022..cb73b4ac9 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts
@@ -4,6 +4,7 @@ import define from '../../../define.js';
 
 export const meta = {
 	tags: ['charts', 'drive', 'users'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(perUserDriveChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts
index f5d42e21c..697a5f37a 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/following.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts
@@ -4,6 +4,7 @@ import { perUserFollowingChart } from '@/services/chart/index.js';
 
 export const meta = {
 	tags: ['charts', 'users', 'following'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(perUserFollowingChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
index aefe550d4..5b576754d 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts
@@ -4,6 +4,7 @@ import define from '../../../define.js';
 
 export const meta = {
 	tags: ['charts', 'users', 'notes'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(perUserNotesChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
index 6bc6b56bf..61c4527b9 100644
--- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts
@@ -4,6 +4,7 @@ import define from '../../../define.js';
 
 export const meta = {
 	tags: ['charts', 'users', 'reactions'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(perUserReactionsChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts
index 338e8fd33..0c799287c 100644
--- a/packages/backend/src/server/api/endpoints/charts/users.ts
+++ b/packages/backend/src/server/api/endpoints/charts/users.ts
@@ -4,6 +4,7 @@ import define from '../../define.js';
 
 export const meta = {
 	tags: ['charts', 'users'],
+	requireCredentialPrivateMode: true,
 
 	res: getJsonSchema(usersChart.schema),
 
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 4ace747ef..eea6f0a0d 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -10,6 +10,7 @@ export const meta = {
 	tags: ['account', 'notes', 'clips'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	kind: 'read:account',
 
diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts
index c3d73c168..aec4c1253 100644
--- a/packages/backend/src/server/api/endpoints/clips/show.ts
+++ b/packages/backend/src/server/api/endpoints/clips/show.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['clips', 'account'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	kind: 'read:account',
 
diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts
index 7b1197d1e..8a04df2d5 100644
--- a/packages/backend/src/server/api/endpoints/federation/followers.ts
+++ b/packages/backend/src/server/api/endpoints/federation/followers.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['federation'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts
index ed1f142d8..fe41eefa4 100644
--- a/packages/backend/src/server/api/endpoints/federation/following.ts
+++ b/packages/backend/src/server/api/endpoints/federation/following.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['federation'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 07e5c07c6..41750f13e 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['federation'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
index 2fbb8a15c..92298f672 100644
--- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts
+++ b/packages/backend/src/server/api/endpoints/federation/show-instance.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['federation'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		oneOf: [{
diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts
index 65ad9f88d..a9b3f3a8c 100644
--- a/packages/backend/src/server/api/endpoints/federation/users.ts
+++ b/packages/backend/src/server/api/endpoints/federation/users.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['federation'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts
index e6acd3691..52232c5cc 100644
--- a/packages/backend/src/server/api/endpoints/gallery/featured.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/featured.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['gallery'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts
index c4c8982fc..5286dcd8b 100644
--- a/packages/backend/src/server/api/endpoints/gallery/popular.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/popular.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['gallery'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts
index 428ba9cc7..f556ec513 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts.ts
@@ -4,6 +4,7 @@ import { GalleryPosts } from '@/models/index.js';
 
 export const meta = {
 	tags: ['gallery'],
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
index 4f6dafd7c..48468f410 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts
@@ -6,6 +6,7 @@ export const meta = {
 	tags: ['gallery'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	errors: {
 		noSuchPost: {
diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
index 56c550297..a8febe05b 100644
--- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts
+++ b/packages/backend/src/server/api/endpoints/get-online-users-count.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['meta'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 } as const;
 
 export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts
index 50e36386c..4b18cb76a 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/list.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/list.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['hashtags'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts
index c28984477..ed1abf1a1 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/search.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['hashtags'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts
index 5b78f6ac7..409233c24 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/show.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/show.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['hashtags'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
index 9cdbc8941..8795927e6 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts
@@ -24,6 +24,7 @@ export const meta = {
 	tags: ['hashtags'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index a5df21a7e..1d18a9ce7 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -4,6 +4,7 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js';
 
 export const meta = {
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	tags: ['hashtags', 'users'],
 
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index ca6b471d5..d93d399e4 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -336,7 +336,7 @@ export default define(meta, paramDef, async (ps, me) => {
 			expiresAt: MoreThan(new Date()),
 		},
 	});
-	// TODO: add secure mode, etc
+
 	const response: any = {
 		maintainerName: instance.maintainerName,
 		maintainerEmail: instance.maintainerEmail,
@@ -350,6 +350,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		tosUrl: instance.ToSUrl,
 		repositoryUrl: instance.repositoryUrl,
 		feedbackUrl: instance.feedbackUrl,
+
+		secureMode: instance.secureMode,
+		privateMode: instance.privateMode,
+
 		disableRegistration: instance.disableRegistration,
 		disableLocalTimeline: instance.disableLocalTimeline,
 		disableGlobalTimeline: instance.disableGlobalTimeline,
@@ -369,10 +373,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		backgroundImageUrl: instance.backgroundImageUrl,
 		logoImageUrl: instance.logoImageUrl,
 		maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
-		emojis: await Emojis.packMany(emojis),
+		emojis: instance.privateMode && !me ? [] : await Emojis.packMany(emojis),
 		defaultLightTheme: instance.defaultLightTheme,
 		defaultDarkTheme: instance.defaultDarkTheme,
-		ads: ads.map(ad => ({
+		ads: instance.privateMode && !me ? [] : ads.map(ad => ({
 			id: ad.id,
 			url: ad.url,
 			place: ad.place,
@@ -390,8 +394,8 @@ export default define(meta, paramDef, async (ps, me) => {
 		translatorAvailable: instance.deeplAuthKey != null,
 
 		...(ps.detail ? {
-			pinnedPages: instance.pinnedPages,
-			pinnedClipId: instance.pinnedClipId,
+			pinnedPages: instance.privateMode && !me ? [] : instance.pinnedPages,
+			pinnedClipId: instance.privateMode && !me ? [] : instance.pinnedClipId,
 			cacheRemoteFiles: instance.cacheRemoteFiles,
 			requireSetup: (await Users.countBy({
 				host: IsNull(),
@@ -400,9 +404,11 @@ export default define(meta, paramDef, async (ps, me) => {
 	};
 
 	if (ps.detail) {
-		const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
+		if (!instance.privateMode || me) {
+			const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
+			response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
+		}
 
-		response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
 		response.features = {
 			registration: !instance.disableRegistration,
 			localTimeLine: !instance.disableLocalTimeline,
diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts
index 015b0338e..fc2bc3741 100644
--- a/packages/backend/src/server/api/endpoints/notes.ts
+++ b/packages/backend/src/server/api/endpoints/notes.ts
@@ -5,6 +5,7 @@ import { makePaginationQuery } from '../common/make-pagination-query.js';
 export const meta = {
 	tags: ['notes'],
 
+	requireCredentialPrivateMode: true,
 	res: {
 		type: 'array',
 		optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts
index efc109105..d27bbaefa 100644
--- a/packages/backend/src/server/api/endpoints/notes/children.ts
+++ b/packages/backend/src/server/api/endpoints/notes/children.ts
@@ -10,6 +10,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
@@ -20,7 +21,7 @@ export const meta = {
 			ref: 'Note',
 		},
 	},
-} as const;
+};
 
 export const paramDef = {
 	type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts
index e79f8563e..5a4420a68 100644
--- a/packages/backend/src/server/api/endpoints/notes/clips.ts
+++ b/packages/backend/src/server/api/endpoints/notes/clips.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['clips', 'notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts
index b731d1824..28613962a 100644
--- a/packages/backend/src/server/api/endpoints/notes/conversation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index dd9cc581a..0e4a454d7 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index 925318f54..6a468f198 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -12,6 +12,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
 export const meta = {
 	tags: ['notes'],
 
+	requireCredentialPrivateMode: true,
 	res: {
 		type: 'array',
 		optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index aac2a3749..3a5c458a0 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -14,6 +14,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
 
 export const meta = {
 	tags: ['notes'],
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index 15a62d394..be2846d25 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['notes', 'reactions'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	allowGet: true,
 	cacheSec: 60,
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index 28be36076..4d0cd8fc6 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -11,6 +11,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts
index ab0018f58..b05ef5914 100644
--- a/packages/backend/src/server/api/endpoints/notes/replies.ts
+++ b/packages/backend/src/server/api/endpoints/notes/replies.ts
@@ -9,6 +9,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
index 777de7221..231913223 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -10,6 +10,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
 
 export const meta = {
 	tags: ['notes', 'hashtags'],
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index 4e2cdae80..cf3de47a3 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -12,6 +12,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts
index 5cd74bd2c..470791b1b 100644
--- a/packages/backend/src/server/api/endpoints/notes/show.ts
+++ b/packages/backend/src/server/api/endpoints/notes/show.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 5e40e7106..ba6e262d6 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -12,6 +12,7 @@ export const meta = {
 	tags: ['notes'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts
index 5a149a626..75580778b 100644
--- a/packages/backend/src/server/api/endpoints/pages/featured.ts
+++ b/packages/backend/src/server/api/endpoints/pages/featured.ts
@@ -5,6 +5,7 @@ export const meta = {
 	tags: ['pages'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts
index 5d37e86b9..54ae43deb 100644
--- a/packages/backend/src/server/api/endpoints/pages/show.ts
+++ b/packages/backend/src/server/api/endpoints/pages/show.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['pages'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'object',
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 41595b47d..d2ded60a1 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -9,6 +9,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index 99f3730e9..fdfbc8a6f 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -4,6 +4,7 @@ import define from '../define.js';
 
 export const meta = {
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	tags: ['meta'],
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts
index cc94f8bf2..0f2fb1f41 100644
--- a/packages/backend/src/server/api/endpoints/stats.ts
+++ b/packages/backend/src/server/api/endpoints/stats.ts
@@ -5,6 +5,7 @@ import { IsNull } from 'typeorm';
 
 export const meta = {
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	tags: ['meta'],
 
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index 3a8211374..d2f2ddcbf 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	res: {
 		type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts
index 09fdf27c2..becfad52d 100644
--- a/packages/backend/src/server/api/endpoints/users/clips.ts
+++ b/packages/backend/src/server/api/endpoints/users/clips.ts
@@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
 
 export const meta = {
 	tags: ['users', 'clips'],
+	requireCredentialPrivateMode: true,
 
 	description: 'Show all clips this user owns.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index 7f9f98076..4971d21b0 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -9,6 +9,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Show everyone that follows this user.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 0aaa810f7..043841aa4 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -9,6 +9,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Show everyone that this user is following.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
index 35bf2df59..95ca77825 100644
--- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts
@@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query.js';
 
 export const meta = {
 	tags: ['users', 'gallery'],
+	requireCredentialPrivateMode: true,
 
 	description: 'Show all gallery posts by the given user.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
index 56965d306..8cf3ea040 100644
--- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -9,6 +9,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Get a list of other users that the specified user frequently replies to.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 9fa56fe83..1e205eec3 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -11,6 +11,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
 export const meta = {
 	tags: ['users', 'notes'],
 
+	requireCredentialPrivateMode: true,
 	description: 'Show all notes that this user created.',
 
 	res: {
diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts
index b1d28af84..e1d876e6b 100644
--- a/packages/backend/src/server/api/endpoints/users/pages.ts
+++ b/packages/backend/src/server/api/endpoints/users/pages.ts
@@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
 
 export const meta = {
 	tags: ['users', 'pages'],
+	requireCredentialPrivateMode: true,
 
 	description: 'Show all pages this user created.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index 9668bd21b..79cf58a41 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['users', 'reactions'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Show all reactions this user made.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 6e5bc46bb..fa1cb8761 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -8,6 +8,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Search for a user by username and/or host.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index 01729de66..70aaa4526 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Search for users.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 846d83b49..892e37bdf 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -10,6 +10,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Show the properties of a user.',
 
diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts
index 47f322ee9..a68b6ea40 100644
--- a/packages/backend/src/server/api/endpoints/users/stats.ts
+++ b/packages/backend/src/server/api/endpoints/users/stats.ts
@@ -7,6 +7,7 @@ export const meta = {
 	tags: ['users'],
 
 	requireCredential: false,
+	requireCredentialPrivateMode: true,
 
 	description: 'Show statistics about a user.',
 

From 4e52c8d8fdb8eff2c81aa73a373af0abbe230750 Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Tue, 20 Jul 2021 13:08:21 -0700
Subject: [PATCH 4/5] Add secure mode settings to Security tab

---
 locales/ja-JP.yml                             |  7 ++++
 .../src/server/api/endpoints/admin/meta.ts    | 19 +++++++++
 .../server/api/endpoints/admin/update-meta.ts | 17 ++++++++
 packages/client/src/pages/admin/security.vue  | 39 +++++++++++++++++++
 4 files changed, 82 insertions(+)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index e071b4bda..c2a71d148 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -778,6 +778,13 @@ middle: "中"
 low: "低"
 emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
 ratio: "比率"
+secureMode: "セキュアモード (Authorized Fetch)"
+instanceSecurity: "インスタンスのセキュリティー"
+secureModeInfo: "他のインスタンスからリクエストするときに、証明を付けなければ返送しません。他のインスタンスの設定ファイルでsignToActivityPubGetはtrueにしてください。"
+privateMode: "非公開モード"
+privateModeInfo: "有効にして、許可されているインスタンスのみがリクエストできます。すべてのノートが公開に非表示にします。"
+allowedInstances: "許可されたインスタンス"
+allowedInstancesDescription: "許可したいインスタンスのホストを改行で区切って設定します。非公開モードだけで有効です。"
 previewNoteText: "本文をプレビュー"
 customCss: "カスタムCSS"
 customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 874611968..8a11baf90 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -187,6 +187,22 @@ export const meta = {
 					optional: false, nullable: false,
 				},
 			},
+			allowedHosts: {
+				type: 'array',
+				optional: true, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
+			privateMode: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+			secureMode: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			hcaptchaSecretKey: {
 				type: 'string',
 				optional: true, nullable: true,
@@ -388,6 +404,9 @@ export default define(meta, paramDef, async (ps, me) => {
 		pinnedUsers: instance.pinnedUsers,
 		hiddenTags: instance.hiddenTags,
 		blockedHosts: instance.blockedHosts,
+		allowedHosts: instance.allowedHosts,
+		privateMode: instance.privateMode,
+		secureMode: instance.secureMode,
 		hcaptchaSecretKey: instance.hcaptchaSecretKey,
 		recaptchaSecretKey: instance.recaptchaSecretKey,
 		sensitiveMediaDetection: instance.sensitiveMediaDetection,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index f14aa4105..1fe68f261 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -27,6 +27,11 @@ export const paramDef = {
 		blockedHosts: { type: 'array', nullable: true, items: {
 			type: 'string',
 		} },
+		allowedHosts: { type: 'array', nullable: true, items: {
+			type: 'string',
+    } },
+		secureMode: { type: 'boolean', nullable: true },
+		privateMode: { type: 'boolean', nullable: true },
 		themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
 		mascotImageUrl: { type: 'string', nullable: true },
 		bannerUrl: { type: 'string', nullable: true },
@@ -142,6 +147,18 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.themeColor = ps.themeColor;
 	}
 
+	if (Array.isArray(ps.allowedHosts)) {
+		set.allowedHosts = ps.allowedHosts.filter(Boolean);
+	}
+
+	if (typeof ps.privateMode === 'boolean') {
+		set.privateMode = ps.privateMode;
+	}
+
+	if (typeof ps.secureMode === 'boolean') {
+		set.secureMode = ps.secureMode;
+	}
+
 	if (ps.mascotImageUrl !== undefined) {
 		set.mascotImageUrl = ps.mascotImageUrl;
 	}
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index 07ee412f3..2ccd9be6c 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -94,6 +94,26 @@
 						<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
 					</div>
 				</FormFolder>
+
+				<FormFolder class="_formBlock">
+					<template #label>{{ i18n.ts.instanceSecurity }}</template>
+
+					<div class="_formRoot">
+						<FormSwitch v-if="!privateMode" v-model="secureMode">
+							<template #label>{{ i18n.ts.secureMode }}</template>
+							<template #caption>{{ i18n.ts.secureModeInfo }}</template>
+						</FormSwitch>
+						<FormSwitch v-model="privateMode">
+							<template #label>{{ i18n.ts.privateMode }}</template>
+							<template #caption>{{ i18n.ts.privateModeInfo }}</template>
+						</FormSwitch>
+						<FormTextarea v-if="privateMode" v-model="allowedHosts">
+							<template #label>{{ i18n.ts.allowedInstances }}</template>
+							<template #caption>{{ i18n.ts.allowedInstancesDescription }}</template>
+						</FormTextarea>
+						<FormButton primary class="_formBlock" @click="saveInstance"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
+					</div>
+				</FormFolder>
 			</div>
 		</FormSuspense>
 	</MkSpacer>
@@ -112,6 +132,7 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormRange from '@/components/form/range.vue';
 import FormInput from '@/components/form/input.vue';
 import FormButton from '@/components/ui/button.vue';
+import FormTextarea from '@/components/form/textarea.vue';
 import * as os from '@/os';
 import { fetchInstance } from '@/instance';
 import { i18n } from '@/i18n';
@@ -127,6 +148,10 @@ let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
 let enableIpLogging: boolean = $ref(false);
 let enableActiveEmailValidation: boolean = $ref(false);
 
+let secureMode: boolean = $ref(false);
+let privateMode: boolean = $ref(false);
+let allowedHosts: string = $ref('');
+
 async function init() {
 	const meta = await os.api('admin/meta');
 	summalyProxy = meta.summalyProxy;
@@ -143,6 +168,10 @@ async function init() {
 	enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
 	enableIpLogging = meta.enableIpLogging;
 	enableActiveEmailValidation = meta.enableActiveEmailValidation;
+
+	secureMode = meta.secureMode;
+	privateMode = meta.privateMode;
+	allowedHosts = meta.allowedHosts.join('\n');
 }
 
 function save() {
@@ -165,6 +194,16 @@ function save() {
 	});
 }
 
+function saveInstance() {
+	os.apiWithDialog('admin/update-meta', {
+		secureMode,
+		privateMode,
+		allowedHosts: allowedHosts.split('\n'),
+	}).then(() => {
+		fetchInstance();
+	});
+}
+
 const headerActions = $computed(() => []);
 
 const headerTabs = $computed(() => []);

From 1df427a08581136bfee4faf86eb3759ed6508e4e Mon Sep 17 00:00:00 2001
From: nullobsi <me@nullob.si>
Date: Wed, 25 Aug 2021 20:48:57 -0700
Subject: [PATCH 5/5] Hide private data in pug when private mode is enabled

---
 packages/backend/src/server/web/index.ts      | 15 ++++++
 .../backend/src/server/web/views/base.pug     |  6 ++-
 .../backend/src/server/web/views/channel.pug  | 16 +++---
 .../backend/src/server/web/views/clip.pug     | 33 ++++++------
 .../src/server/web/views/gallery-post.pug     | 35 +++++++------
 .../backend/src/server/web/views/note.pug     | 51 ++++++++++---------
 .../backend/src/server/web/views/page.pug     | 33 ++++++------
 .../backend/src/server/web/views/user.pug     | 47 +++++++++--------
 8 files changed, 135 insertions(+), 101 deletions(-)

diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index be95becb6..0968f3271 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -218,6 +218,10 @@ router.get('/api.json', async ctx => {
 });
 
 const getFeed = async (acct: string) => {
+	const meta = await fetchMeta();
+	if (meta.privateMode) {
+		return;
+	}
 	const { username, host } = Acct.parse(acct);
 	const user = await Users.findOneBy({
 		usernameLower: username.toLowerCase(),
@@ -290,6 +294,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl,
 			themeColor: meta.themeColor,
+			privateMode: meta.privateMode,
 		});
 		ctx.set('Cache-Control', 'public, max-age=15');
 	} else {
@@ -333,6 +338,7 @@ router.get('/notes/:note', async (ctx, next) => {
 			summary: getNoteSummary(_note),
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl,
+			privateMode: meta.privateMode,
 			themeColor: meta.themeColor,
 		});
 
@@ -370,6 +376,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl,
 			themeColor: meta.themeColor,
+			privateMode: meta.privateMode,
 		});
 
 		if (['public'].includes(page.visibility)) {
@@ -400,6 +407,7 @@ router.get('/clips/:clip', async (ctx, next) => {
 			profile,
 			avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: clip.userId })),
 			instanceName: meta.name || 'Misskey',
+			privateMode: meta.privateMode,
 			icon: meta.iconUrl,
 			themeColor: meta.themeColor,
 		});
@@ -427,6 +435,7 @@ router.get('/gallery/:post', async (ctx, next) => {
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl,
 			themeColor: meta.themeColor,
+			privateMode: meta.privateMode,
 		});
 
 		ctx.set('Cache-Control', 'public, max-age=15');
@@ -451,6 +460,7 @@ router.get('/channels/:channel', async (ctx, next) => {
 			instanceName: meta.name || 'Misskey',
 			icon: meta.iconUrl,
 			themeColor: meta.themeColor,
+			privateMode: meta.privateMode,
 		});
 
 		ctx.set('Cache-Control', 'public, max-age=15');
@@ -464,6 +474,10 @@ router.get('/channels/:channel', async (ctx, next) => {
 
 router.get('/_info_card_', async ctx => {
 	const meta = await fetchMeta(true);
+	if (meta.privateMode) {
+		ctx.status = 403;
+		return;
+	}
 
 	ctx.remove('X-Frame-Options');
 
@@ -511,6 +525,7 @@ router.get('(.*)', async ctx => {
 		desc: meta.description,
 		icon: meta.iconUrl,
 		themeColor: meta.themeColor,
+		privateMode: meta.privateMode,
 	});
 	ctx.set('Cache-Control', 'public, max-age=15');
 });
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 5bb156f0f..effa0743b 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -51,10 +51,12 @@ html
 			meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
 
 		block meta
+			if privateMode
+				meta(name='robots' content='noindex')
 
 		block og
-			meta(property='og:title'       content= title || 'Misskey') 
-			meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨') 
+			meta(property='og:title'       content= title || 'Misskey')
+			meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
 			meta(property='og:image'       content= img)
 
 		style
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
index 486f0ecc4..fd5da0deb 100644
--- a/packages/backend/src/server/web/views/channel.pug
+++ b/packages/backend/src/server/web/views/channel.pug
@@ -1,18 +1,20 @@
 extends ./base
 
 block vars
-	- const title = channel.name;
+	- const title = privateMode ? '非公開インスタンス' : channel.name;
 	- const url = `${config.url}/channels/${channel.id}`;
 
 block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= channel.description)
+	unless privateMode
+		meta(name='description' content=channel.description)
 
 block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= channel.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= channel.bannerUrl)
+	unless privateMode
+		meta(property='og:type'        content='article')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= channel.description)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= channel.bannerUrl)
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
index 4c692bf59..5247b56af 100644
--- a/packages/backend/src/server/web/views/clip.pug
+++ b/packages/backend/src/server/web/views/clip.pug
@@ -2,30 +2,33 @@ extends ./base
 
 block vars
 	- const user = clip.user;
-	- const title = clip.name;
+	- const title = privateMode ? '非公開インスタンス' : clip.name;
 	- const url = `${config.url}/clips/${clip.id}`;
 
 block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= clip.description)
+	unless privateMode
+		meta(name='description' content= clip.description)
 
 block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= clip.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= avatarUrl)
+	unless privateMode
+		meta(property='og:type'        content='article')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= clip.description)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= avatarUrl)
 
 block meta
-	if profile.noCrawle
-		meta(name='robots' content='noindex')
+	unless privateMode
+		if profile.noCrawle
+			meta(name='robots' content='noindex')
 
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:clip-id' content=clip.id)
+		meta(name='misskey:user-username' content=user.username)
+		meta(name='misskey:user-id' content=user.id)
+		meta(name='misskey:clip-id' content=clip.id)
 
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+		// todo
+		if user.twitter
+			meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
index ca0663a48..1bbe740b2 100644
--- a/packages/backend/src/server/web/views/gallery-post.pug
+++ b/packages/backend/src/server/web/views/gallery-post.pug
@@ -2,32 +2,35 @@ extends ./base
 
 block vars
 	- const user = post.user;
-	- const title = post.title;
+	- const title = privateMode ? '非公開インスタンス' : post.title;
 	- const url = `${config.url}/gallery/${post.id}`;
 
 block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= post.description)
+	unless privateMode
+		meta(name='description' content= post.description)
 
 block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= post.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= post.files[0].thumbnailUrl)
+	unless privateMode
+		meta(property='og:type'        content='article')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= post.description)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= post.files[0].thumbnailUrl)
 
 block meta
-	if user.host || profile.noCrawle
-		meta(name='robots' content='noindex')
+	unless privateMode
+		if user.host || profile.noCrawle
+			meta(name='robots' content='noindex')
 
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
+		meta(name='misskey:user-username' content=user.username)
+		meta(name='misskey:user-id' content=user.id)
 
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+		// todo
+		if user.twitter
+			meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 
-	if !user.host
-		link(rel='alternate' href=url type='application/activity+json')
+		if !user.host
+			link(rel='alternate' href=url type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index 65696ea13..1e6efa375 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -2,7 +2,7 @@ extends ./base
 
 block vars
 	- const user = note.user;
-	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+	- const title = privateMode ? '非公開インスタンス' : (user.name ? `${user.name} (@${user.username})` : `@${user.username}`);
 	- const url = `${config.url}/notes/${note.id}`;
 	- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
 
@@ -10,33 +10,36 @@ block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= summary)
+	unless privateMode
+		meta(name='description' content= summary)
 
 block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= summary)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= avatarUrl)
+	unless privateMode
+		meta(property='og:type'        content='article')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= summary)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= avatarUrl)
 
 block meta
-	if user.host || isRenote || profile.noCrawle
-		meta(name='robots' content='noindex')
+	unless privateMode
+		if user.host || isRenote || profile.noCrawle
+			meta(name='robots' content='noindex')
 
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:note-id' content=note.id)
-	
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+		meta(name='misskey:user-username' content=user.username)
+		meta(name='misskey:user-id' content=user.id)
+		meta(name='misskey:note-id' content=note.id)
 
-	if note.prev
-		link(rel='prev' href=`${config.url}/notes/${note.prev}`)
-	if note.next
-		link(rel='next' href=`${config.url}/notes/${note.next}`)
+		// todo
+		if user.twitter
+			meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 
-	if !user.host
-		link(rel='alternate' href=url type='application/activity+json')
-	if note.uri
-		link(rel='alternate' href=note.uri type='application/activity+json')
+		if note.prev
+			link(rel='prev' href=`${config.url}/notes/${note.prev}`)
+		if note.next
+			link(rel='next' href=`${config.url}/notes/${note.next}`)
+
+		if !user.host
+			link(rel='alternate' href=url type='application/activity+json')
+		if note.uri
+			link(rel='alternate' href=note.uri type='application/activity+json')
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index 4219e76a5..3378562d4 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -2,30 +2,33 @@ extends ./base
 
 block vars
 	- const user = page.user;
-	- const title = page.title;
+	- const title = privateMode ? '非公開インスタンス' : page.title;
 	- const url = `${config.url}/@${user.username}/${page.name}`;
 
 block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= page.summary)
+	unless privateMode
+		meta(name='description' content= page.summary)
 
 block og
-	meta(property='og:type'        content='article')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= page.summary)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
+	unless privateMode
+		meta(property='og:type'        content='article')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= page.summary)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
 
 block meta
-	if profile.noCrawle
-		meta(name='robots' content='noindex')
+	unless privateMode
+		if profile.noCrawle
+			meta(name='robots' content='noindex')
 
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
-	meta(name='misskey:page-id' content=page.id)
+		meta(name='misskey:user-username' content=user.username)
+		meta(name='misskey:user-id' content=user.id)
+		meta(name='misskey:page-id' content=page.id)
 
-	// todo
-	if user.twitter
-		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
+		// todo
+		if user.twitter
+			meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
index 119993fdb..d52a92694 100644
--- a/packages/backend/src/server/web/views/user.pug
+++ b/packages/backend/src/server/web/views/user.pug
@@ -1,39 +1,42 @@
 extends ./base
 
 block vars
-	- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+	- const title = privateMode ? '非公開インスタンス' : (user.name ? `${user.name} (@${user.username})` : `@${user.username}`);
 	- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
 
 block title
 	= `${title} | ${instanceName}`
 
 block desc
-	meta(name='description' content= profile.description)
+	unless privateMode
+		meta(name='description' content= profile.description)
 
 block og
-	meta(property='og:type'        content='blog')
-	meta(property='og:title'       content= title)
-	meta(property='og:description' content= profile.description)
-	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= avatarUrl)
+	unless privateMode
+		meta(property='og:type'        content='blog')
+		meta(property='og:title'       content= title)
+		meta(property='og:description' content= profile.description)
+		meta(property='og:url'         content= url)
+		meta(property='og:image'       content= avatarUrl)
 
 block meta
-	if user.host || profile.noCrawle
-		meta(name='robots' content='noindex')
+	unless privateMode
+		if user.host || profile.noCrawle
+			meta(name='robots' content='noindex')
 
-	meta(name='misskey:user-username' content=user.username)
-	meta(name='misskey:user-id' content=user.id)
+		meta(name='misskey:user-username' content=user.username)
+		meta(name='misskey:user-id' content=user.id)
 
-	if profile.twitter
-		meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
+		if profile.twitter
+			meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
 
-	if !sub
-		if !user.host
-			link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
-		if user.uri
-			link(rel='alternate' href=user.uri type='application/activity+json')
-		if profile.url
-			link(rel='alternate' href=profile.url type='text/html')
+		if !sub
+			if !user.host
+				link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
+			if user.uri
+				link(rel='alternate' href=user.uri type='application/activity+json')
+			if profile.url
+				link(rel='alternate' href=profile.url type='text/html')
 
-	each m in me
-		link(rel='me' href=`${m}`)
+		each m in me
+			link(rel='me' href=`${m}`)