implement CSP, remove commercial supporters from about section
Too much diff don't want to deal with it Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
e75d885a2c
commit
67197fbb76
11 changed files with 202 additions and 46 deletions
|
@ -1,3 +1,8 @@
|
||||||
|
## 2024.11.0-yumechinokuni.2
|
||||||
|
|
||||||
|
- Security: CSPの設定を強化
|
||||||
|
- Fix: flaky testの修正
|
||||||
|
|
||||||
## 2024.11.0
|
## 2024.11.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { dirname, resolve } from 'node:path';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import type { RedisOptions } from 'ioredis';
|
import type { RedisOptions } from 'ioredis';
|
||||||
|
import { type CSPHashed, hashResource, hashSourceFile } from './server/csp.js';
|
||||||
|
|
||||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -154,6 +155,8 @@ export type Config = {
|
||||||
proxyRemoteFiles: boolean | undefined;
|
proxyRemoteFiles: boolean | undefined;
|
||||||
signToActivityPubGet: boolean | undefined;
|
signToActivityPubGet: boolean | undefined;
|
||||||
|
|
||||||
|
cspPrerenderedContent: Map<string, CSPHashed>,
|
||||||
|
|
||||||
version: string;
|
version: string;
|
||||||
gitDescribe: string;
|
gitDescribe: string;
|
||||||
gitCommit: string;
|
gitCommit: string;
|
||||||
|
@ -235,6 +238,15 @@ export function loadConfig(): Config {
|
||||||
: null;
|
: null;
|
||||||
const internalMediaProxy = `${scheme}://${host}/proxy`;
|
const internalMediaProxy = `${scheme}://${host}/proxy`;
|
||||||
const redis = convertRedisOptions(config.redis, host);
|
const redis = convertRedisOptions(config.redis, host);
|
||||||
|
const htmlScriptPrelude = `var VERSION = ${JSON.stringify(version)}; var CLIENT_ENTRY = ${JSON.stringify(frontendManifest['src/_boot_.ts'].file)};`;
|
||||||
|
const cspPrerenderedContent = new Map([
|
||||||
|
[
|
||||||
|
'.prelude.js', hashResource(htmlScriptPrelude)
|
||||||
|
],
|
||||||
|
...['boot.js', 'style.css', 'style.embed.css', 'boot.embed.js',
|
||||||
|
'bios.css', 'bios.js', 'cli.css', 'cli.js', 'error.css'
|
||||||
|
].map((file) => [file, hashSourceFile(`${_dirname}/server/web/${file}`)] as [string, CSPHashed]),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
|
@ -248,6 +260,7 @@ export function loadConfig(): Config {
|
||||||
chmodSocket: config.chmodSocket,
|
chmodSocket: config.chmodSocket,
|
||||||
disableHsts: config.disableHsts,
|
disableHsts: config.disableHsts,
|
||||||
hstsPreload: config.hstsPreload ?? false,
|
hstsPreload: config.hstsPreload ?? false,
|
||||||
|
cspPrerenderedContent,
|
||||||
host,
|
host,
|
||||||
hostname,
|
hostname,
|
||||||
scheme,
|
scheme,
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { ClientServerService } from './web/ClientServerService.js';
|
||||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
import { makeHstsHook } from './hsts.js';
|
import { makeHstsHook } from './hsts.js';
|
||||||
|
import { generateCSP } from './csp.js';
|
||||||
|
|
||||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
@ -88,6 +89,18 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
||||||
|
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
|
||||||
|
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
|
||||||
|
});
|
||||||
|
fastify.addHook('onRequest', (_, reply, done) => {
|
||||||
|
reply.header('Content-Security-Policy', generatedCSP);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register raw-body parser for ActivityPub HTTP signature validation.
|
// Register raw-body parser for ActivityPub HTTP signature validation.
|
||||||
await fastify.register(fastifyRawBody, {
|
await fastify.register(fastifyRawBody, {
|
||||||
global: false,
|
global: false,
|
||||||
|
|
76
packages/backend/src/server/csp.ts
Normal file
76
packages/backend/src/server/csp.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
export type CSPHashed = {
|
||||||
|
content: string,
|
||||||
|
integrity: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateCSP(hashedMap: Map<string, CSPHashed>, options: {
|
||||||
|
mediaProxy?: string,
|
||||||
|
}) {
|
||||||
|
const keys = Array.from(hashedMap.keys());
|
||||||
|
const scripts = keys
|
||||||
|
.filter(name => name.endsWith('.js'))
|
||||||
|
.map(name => `'${hashedMap.get(name)!.integrity}'`);
|
||||||
|
const styles = keys
|
||||||
|
.filter(name => name.endsWith('.css'))
|
||||||
|
.map(name => `'${hashedMap.get(name)!.integrity}'`);
|
||||||
|
|
||||||
|
return ([
|
||||||
|
['default-src', ['\'self\'']],
|
||||||
|
['img-src',
|
||||||
|
[
|
||||||
|
'\'self\'',
|
||||||
|
'data:',
|
||||||
|
// 'https://avatars.githubusercontent.com', // uncomment this for contributor avatars to work
|
||||||
|
options.mediaProxy
|
||||||
|
].filter(Boolean)],
|
||||||
|
['media-src', ['\'self\'', 'data:', options.mediaProxy].filter(Boolean)],
|
||||||
|
['font-src', ['\'self\'']],
|
||||||
|
['style-src', ['\'self\'', ...styles]],
|
||||||
|
// remove below line if you do not use code highlighting.
|
||||||
|
// shiki is not designed to work with CSP, so we need to allow unsafe-inline for now.
|
||||||
|
//
|
||||||
|
// Allowing inline style attributes is a very small risk, so I will allow by default.
|
||||||
|
// Since you can not write CSS selectors or cascading rules in the inline style attributes.
|
||||||
|
//
|
||||||
|
// ref: https://github.com/shikijs/shiki/issues/671
|
||||||
|
['style-src-attr', ['\'self\'', '\'unsafe-inline\'']],
|
||||||
|
['script-src', ['\'self\'', '\'wasm-unsafe-eval\'', ...scripts]],
|
||||||
|
['object-src', ['\'none\'']],
|
||||||
|
['frame-src', ['\'none\'']],
|
||||||
|
['base-uri', ['\'self\'']],
|
||||||
|
['form-action', ['\'self\'']],
|
||||||
|
['child-src', ['\'self\'']],
|
||||||
|
['manifest-src', ['\'self\'']],
|
||||||
|
...(process.env.NODE_ENV === 'production' ?
|
||||||
|
[
|
||||||
|
['upgrade-insecure-requests', []],
|
||||||
|
] : []),
|
||||||
|
] as [string, string[]][])
|
||||||
|
.map(([name, values]) => {
|
||||||
|
return `${name} ${values.join(' ')}`;
|
||||||
|
}).join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashResource(res: string): CSPHashed {
|
||||||
|
const sha256 = createHash('sha256');
|
||||||
|
|
||||||
|
sha256.update(res);
|
||||||
|
|
||||||
|
const content = res;
|
||||||
|
const integrity = `sha256-${sha256.digest('base64')}`;
|
||||||
|
return { content, integrity };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashSourceFile(file: string): CSPHashed {
|
||||||
|
const content = readFileSync(file, 'utf8');
|
||||||
|
|
||||||
|
return hashResource(content);
|
||||||
|
}
|
|
@ -67,6 +67,8 @@ import { UrlPreviewService } from './UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||||
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
|
import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify';
|
||||||
import { makeHstsHook } from '../hsts.js';
|
import { makeHstsHook } from '../hsts.js';
|
||||||
|
import { generateCSP } from '../csp.js';
|
||||||
|
import { appendQuery, query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
@ -204,6 +206,17 @@ export class ClientServerService {
|
||||||
return (manifest);
|
return (manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string {
|
||||||
|
return appendQuery(
|
||||||
|
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
||||||
|
query({
|
||||||
|
url,
|
||||||
|
...(mode ? { [mode]: '1' } : {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async generateCommonPugData(meta: MiMeta) {
|
private async generateCommonPugData(meta: MiMeta) {
|
||||||
return {
|
return {
|
||||||
|
@ -211,9 +224,9 @@ export class ClientServerService {
|
||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
appleTouchIcon: meta.app512IconUrl,
|
appleTouchIcon: meta.app512IconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
serverErrorImageUrl: meta.serverErrorImageUrl ?? this.getProxiedUrl('https://xn--931a.moe/assets/error.jpg'),
|
||||||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
infoImageUrl: meta.infoImageUrl ?? this.getProxiedUrl('https://xn--931a.moe/assets/info.jpg'),
|
||||||
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
|
notFoundImageUrl: meta.notFoundImageUrl ?? this.getProxiedUrl('https://xn--931a.moe/assets/not-found.jpg'),
|
||||||
instanceUrl: this.config.url,
|
instanceUrl: this.config.url,
|
||||||
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
|
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
|
||||||
now: Date.now(),
|
now: Date.now(),
|
||||||
|
@ -234,6 +247,18 @@ export class ClientServerService {
|
||||||
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
fastify.addHook('onRequest', makeHstsHook(host, preload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CSP
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.debug('cspPrerenderedContent', this.config.cspPrerenderedContent);
|
||||||
|
const generatedCSP = generateCSP(this.config.cspPrerenderedContent, {
|
||||||
|
mediaProxy: this.config.mediaProxy ? `https://${new URL(this.config.mediaProxy).host}` : undefined,
|
||||||
|
});
|
||||||
|
fastify.addHook('onRequest', (_, reply, done) => {
|
||||||
|
reply.header('Content-Security-Policy', generatedCSP);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate
|
// Authenticate
|
||||||
fastify.addHook('onRequest', async (request, reply) => {
|
fastify.addHook('onRequest', async (request, reply) => {
|
||||||
if (request.routeOptions.url == null) {
|
if (request.routeOptions.url == null) {
|
||||||
|
@ -296,6 +321,20 @@ export class ClientServerService {
|
||||||
version: this.config.version,
|
version: this.config.version,
|
||||||
config: this.config,
|
config: this.config,
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
filters: {
|
||||||
|
dataTag: (data: string, options: { tagName: string, mimeType: string }) => {
|
||||||
|
if (!/^[a-z]+$/.test(options.tagName)) {
|
||||||
|
throw new Error('Invalid tagName');
|
||||||
|
}
|
||||||
|
if (/[;'"]/.test(options.mimeType)) {
|
||||||
|
throw new Error('Invalid mimeType');
|
||||||
|
}
|
||||||
|
const dataURI = `data:${options.mimeType};base64,${Buffer.from(data).toString('base64')}`;
|
||||||
|
return `<${options.tagName} data="${dataURI}"></${options.tagName}>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.addHook('onRequest', (request, reply, done) => {
|
fastify.addHook('onRequest', (request, reply, done) => {
|
||||||
|
@ -619,7 +658,7 @@ export class ClientServerService {
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
|
reply.redirect(`/@${user.username}${user.host == null ? '' : '@' + user.host}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note
|
// Note
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
block vars
|
block vars
|
||||||
|
|
||||||
block loadClientEntry
|
block loadClientEntry
|
||||||
- const entry = config.frontendEmbedEntry;
|
- const entry = config.frontendEntry
|
||||||
|
- const styleCSS = config.cspPrerenderedContent['style.embed.css']
|
||||||
|
- const bootJS = config.cspPrerenderedContent['boot.embed.js']
|
||||||
|
- const jsPrelude = config.cspPrerenderedContent['baseHtmlJSPrelude']
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
|
@ -35,12 +38,9 @@ html(class='embed')
|
||||||
block meta
|
block meta
|
||||||
meta(name='robots' content='noindex')
|
meta(name='robots' content='noindex')
|
||||||
|
|
||||||
style
|
style(integrity=styleCSS.integrity) !{styleCSS.content}
|
||||||
include ../style.embed.css
|
|
||||||
|
|
||||||
script.
|
script(integrity=jsPrelude.integrity) !{jsPrelude.content}
|
||||||
var VERSION = "#{version}";
|
|
||||||
var CLIENT_ENTRY = "#{entry.file}";
|
|
||||||
|
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
@ -48,8 +48,7 @@ html(class='embed')
|
||||||
script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
|
script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
|
||||||
!= embedCtx
|
!= embedCtx
|
||||||
|
|
||||||
script
|
script(integrity=bootJS.integrity) !{bootJS.content}
|
||||||
include ../boot.embed.js
|
|
||||||
|
|
||||||
body
|
body
|
||||||
noscript: p
|
noscript: p
|
||||||
|
@ -59,11 +58,13 @@ html(class='embed')
|
||||||
div#splash
|
div#splash
|
||||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||||
div#splashSpinner
|
div#splashSpinner
|
||||||
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
<g transform="matrix(1,0,0,1,12,12)">
|
||||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
<g transform="matrix(1,0,0,1,12,12)">
|
||||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||||
|
|
|
@ -2,7 +2,9 @@ block vars
|
||||||
|
|
||||||
block loadClientEntry
|
block loadClientEntry
|
||||||
- const entry = config.frontendEntry;
|
- const entry = config.frontendEntry;
|
||||||
- const baseUrl = config.url;
|
- const styleCSS = config.cspPrerenderedContent.get('style.css');
|
||||||
|
- const jsPrelude = config.cspPrerenderedContent.get('.prelude.js');
|
||||||
|
- const bootJS = config.cspPrerenderedContent.get('boot.js');
|
||||||
|
|
||||||
doctype html
|
doctype html
|
||||||
|
|
||||||
|
@ -64,18 +66,14 @@ html
|
||||||
meta(property='og:image' content= img)
|
meta(property='og:image' content= img)
|
||||||
meta(property='twitter:card' content='summary')
|
meta(property='twitter:card' content='summary')
|
||||||
|
|
||||||
style
|
style(integrity=styleCSS.integrity) !{styleCSS.content}
|
||||||
include ../style.css
|
|
||||||
|
|
||||||
script.
|
script(integrity=jsPrelude.integrity) !{jsPrelude.content}
|
||||||
var VERSION = "#{version}";
|
|
||||||
var CLIENT_ENTRY = "#{entry.file}";
|
|
||||||
|
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
|
||||||
script
|
script(integrity=bootJS.integrity) !{bootJS.content}
|
||||||
include ../boot.js
|
|
||||||
|
|
||||||
body
|
body
|
||||||
noscript: p
|
noscript: p
|
||||||
|
@ -85,11 +83,13 @@ html
|
||||||
div#splash
|
div#splash
|
||||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||||
div#splashSpinner
|
div#splashSpinner
|
||||||
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
<g transform="matrix(1,0,0,1,12,12)">
|
||||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
:dataTag(tagName='img' mimeType='image/svg+xml')
|
||||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(1,0,0,1,12,12)">
|
<g transform="matrix(1,0,0,1,12,12)">
|
||||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
|
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
|
||||||
|
import { getProxiedImageUrl } from './scripts/media-proxy.js';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
|
@ -30,11 +31,11 @@ if (providedAt > cachedAt) {
|
||||||
|
|
||||||
export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {});
|
export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {});
|
||||||
|
|
||||||
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
|
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? getProxiedImageUrl(DEFAULT_SERVER_ERROR_IMAGE_URL));
|
||||||
|
|
||||||
export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
|
export const infoImageUrl = computed(() => instance.infoImageUrl ?? getProxiedImageUrl(DEFAULT_INFO_IMAGE_URL));
|
||||||
|
|
||||||
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
|
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? getProxiedImageUrl(DEFAULT_NOT_FOUND_IMAGE_URL));
|
||||||
|
|
||||||
export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
|
export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
<!--
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>Special thanks</template>
|
<template #label>Special thanks</template>
|
||||||
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
|
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
|
||||||
|
@ -111,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
-->
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
|
||||||
<div :class="$style.patronsWithIcon">
|
<div :class="$style.patronsWithIcon">
|
||||||
|
@ -145,6 +147,7 @@ import * as os from '@/os.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
const patronsWithIcon = [{
|
const patronsWithIcon = [{
|
||||||
name: 'カイヤン',
|
name: 'カイヤン',
|
||||||
|
@ -272,7 +275,10 @@ const patronsWithIcon = [{
|
||||||
}, {
|
}, {
|
||||||
name: 'Yatoigawa',
|
name: 'Yatoigawa',
|
||||||
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
||||||
}];
|
}].map(patron => ({
|
||||||
|
...patron,
|
||||||
|
icon: getProxiedImageUrl(patron.icon, 'avatar'),
|
||||||
|
}));
|
||||||
|
|
||||||
const patrons = [
|
const patrons = [
|
||||||
'まっちゃとーにゅ',
|
'まっちゃとーにゅ',
|
||||||
|
@ -552,6 +558,7 @@ definePageMetadata(() => ({
|
||||||
}
|
}
|
||||||
|
|
||||||
.contributorAvatar {
|
.contributorAvatar {
|
||||||
|
display: none; // CSP
|
||||||
width: 30px;
|
width: 30px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createHighlighterCore, loadWasm } from 'shiki/core';
|
import { createHighlighterCore } from 'shiki/core';
|
||||||
|
import { loadWasm } from 'shiki/engine/oniguruma';
|
||||||
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
import darkPlus from 'shiki/themes/dark-plus.mjs';
|
||||||
import { bundledThemesInfo } from 'shiki/themes';
|
import { bundledThemesInfo } from 'shiki/themes';
|
||||||
import { bundledLanguagesInfo } from 'shiki/langs';
|
import { bundledLanguagesInfo } from 'shiki/langs';
|
||||||
|
|
|
@ -140,7 +140,7 @@ export function getConfig(): UserConfig {
|
||||||
input: {
|
input: {
|
||||||
app: './src/_boot_.ts',
|
app: './src/_boot_.ts',
|
||||||
},
|
},
|
||||||
external: externalPackages.map(p => p.match),
|
// external: externalPackages.map(p => p.match),
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
vue: ['vue'],
|
vue: ['vue'],
|
||||||
|
|
Loading…
Reference in a new issue