1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-06-24 12:54:47 +02:00

Compare commits

...

22 Commits

Author SHA1 Message Date
github-actions[bot]
4aa1d9ffc8 Release: 2026.5.4 2026-05-21 00:31:56 +00:00
anatawa12
3191f8a72d Merge commit from fork
This issue was originally reported by sururu-k as part of a series of ai slop public pull requests.
Although the original pull request was closed as ai slop, I later confirmed one described a real security issue.
2026-05-21 08:50:43 +09:00
github-actions[bot]
507f3e9870 Bump version to 2026.5.4-beta.0 2026-05-20 13:54:24 +00:00
かっこかり
e400731bbe fix(backend): fix typo [ci skip] 2026-05-20 22:44:45 +09:00
かっこかり
98d362df23 Update theme.ts 2026-05-20 22:35:04 +09:00
かっこかり
f69b3b8d91 Update CHANGELOG.md 2026-05-20 22:15:55 +09:00
github-actions[bot]
f7c233fe9c Bump version to 2026.5.4-alpha.0 2026-05-20 13:14:34 +00:00
かっこかり
602a46cb78 Merge commit from fork
* fix(frontend): avoid recursive reference on theme variables

* fix(theme): filter compiled theme properties to include only valid themeProps
2026-05-20 22:05:15 +09:00
かっこかり
04f18fe919 Merge commit from fork
* fix(backend): restrict chat room / chat message permissions

* spec: モデレーター以上の権限では全てを閲覧可能
2026-05-20 22:03:53 +09:00
Julia Johannesen
6c40c96369 Merge commit from fork
* fix: Prevent timing attacks and RDF-graph rewrites

* fix: Proper vuln fix, not a bandaid

* fix: Accidental removal

* fix: Explicitly check for null

* fix: Address issues

* clean up

* lint fixes

* fix: reset pnpm-lock.yaml to current develop

---------

Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2026-05-20 22:02:25 +09:00
renovate[bot]
408e94f41f fix(deps): update dependency ws to v8.20.1 [security] (#17430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-20 07:46:05 +09:00
おさむのひと
2fe60e6429 chore: set nodeMaxMemory to 4096 in renovate configuration (#17437) 2026-05-19 22:25:30 +09:00
anatawa12
3a27ae0757 fix: false positive not exists error if sourceCode is empty (#17434)
* fix: false positive not exists error if sourceCode is empty

* Return empty array for empty sourceCode

* lint

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2026-05-19 15:55:53 +09:00
anatawa12
af73d795e0 fix: empty filesa are treated as nonexisting files (#17433) 2026-05-19 15:08:42 +09:00
github-actions[bot]
e613120d30 [skip ci] Update CHANGELOG.md (prepend template) 2026-05-18 01:44:56 +00:00
github-actions[bot]
8a38a05d83 Release: 2026.5.3 2026-05-18 01:44:50 +00:00
github-actions[bot]
5b8a38cde8 Bump version to 2026.5.3-alpha.0 2026-05-18 01:24:32 +00:00
かっこかり
d503b8d073 fix(docker): runnerでのpnpmの依存関係チェックを無効化 (#17425)
* fix(docker): runnerでのpnpmの依存関係チェックを無効化

* Update Changelog

* update changelog
2026-05-18 10:23:47 +09:00
syuilo
419cdcff36 Update about-misskey.vue 2026-05-18 07:25:03 +09:00
github-actions[bot]
badb243021 [skip ci] Update CHANGELOG.md (prepend template) 2026-05-17 22:15:01 +00:00
github-actions[bot]
2bc0ccb108 Release: 2026.5.2 2026-05-17 22:14:54 +00:00
おさむのひと
fc6c45d175 fix: add-i18n-keyの記述が間違っていたので修正 (#17418) 2026-05-17 19:30:35 +09:00
18 changed files with 253 additions and 63 deletions

View File

@@ -26,7 +26,9 @@ _settings:
general: "全般"
appearance: "外観"
# パラメータ付き (ICU MessageFormat 互換)
# パラメータ付き (単純なプレースホルダ置換)
# ICU MessageFormat の plural / select / number / date などは非対応
# 使えるのは `{name}` のような単純な置換のみ
greeting: "こんにちは、{name}さん"
```

View File

@@ -1,3 +1,21 @@
## 2026.5.4
### General
- セキュリティに関する修正
### Client
- Fix: ビルドに失敗することがある問題を修正
### Server
-
## 2026.5.3
### General
- Fix: Dockerで起動に失敗する問題を修正
## 2026.5.2
### Note

View File

@@ -74,6 +74,8 @@ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
ARG UID="991"
ARG GID="991"
ENV PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg tini curl libjemalloc-dev libjemalloc2 \

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2026.5.2-beta.1",
"version": "2026.5.4",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -153,7 +153,7 @@
"ulid": "3.0.2",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.20.0",
"ws": "8.20.1",
"xev": "3.0.2"
},
"devDependencies": {

View File

@@ -182,11 +182,12 @@ export class AnnouncementService {
@bindThis
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
if (me) {
if (announcement.userId && announcement.userId !== me.id) {
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
}
if (announcement.userId && (me == null || announcement.userId !== me.id)) {
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
}
if (me) {
const read = await this.announcementReadsRepository.findOneBy({
announcementId: announcement.id,
userId: me.id,

View File

@@ -572,6 +572,27 @@ export class ChatService {
return created;
}
@bindThis
public async hasPermissionToViewRoomInfo(meId: MiUser['id'], room: MiChatRoom) {
if (room.ownerId === meId) {
return true;
}
if (await this.isRoomMember(room, meId)) {
return true;
}
if (await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: meId })) {
return true;
}
if (await this.roleService.isModerator({ id: meId })) {
return true;
}
return false;
}
@bindThis
public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) {
if (room.ownerId === meId) {

View File

@@ -9,6 +9,7 @@ import { Injectable } from '@nestjs/common';
import { RsaKeyPair } from 'slacc';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
import type { JsonLdDocument } from 'jsonld';
@@ -16,7 +17,40 @@ import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
class JsonLd {
export class JsonLdError extends IdentifiableError {
constructor(id: string, message?: string) {
super(id, message);
}
}
export class JsonLdCacheOverflowError extends JsonLdError {
constructor() {
super('42fb039c-69fb-4f75-8187-d3aee412423e', 'context cache overflow');
}
}
export class JsonLdCacheFrozenError extends JsonLdError {
constructor() {
super('202c41fa-72d5-4e22-95af-94a8ac83346f', 'attempt to insert into frozen context cache');
}
}
export class JsonLdForbiddenDirectiveError extends JsonLdError {
constructor(public directive: string) {
super('0297f79b-0ed9-4b6c-875f-b0a82ff96781', `${directive} is forbidden by Misskey in ActivityPub documents`);
}
}
export class JsonLd {
private static forbiddenDirectives = new Set([
'@included',
'@graph',
'@reverse',
]);
private frozen = false;
private cache: Map<string, RemoteDocument> = new Map();
public debug = false;
public preLoad = true;
public loderTimeout = 5000;
@@ -81,9 +115,9 @@ class JsonLd {
const optionsHash = this.sha256(canonizedOptions.toString());
const transformedData = { ...data };
delete transformedData['signature'];
const cannonidedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
const documentHash = this.sha256(cannonidedData.toString());
const cannonizedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonizedData: ${cannonizedData}`);
const documentHash = this.sha256(cannonizedData.toString());
const verifyData = `${optionsHash}${documentHash}`;
return verifyData;
}
@@ -106,6 +140,34 @@ class JsonLd {
});
}
/**
* Prevent any further HTTP requests from being made for the sake of
* validating JSON-LD signatures.
*/
@bindThis
public freeze(): void { this.frozen = true; }
@bindThis
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public checkForForbiddenDirectives(value: any): void {
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
for (const item of value) this.checkForForbiddenDirectives(item);
} else {
const object = value;
for (const [key, value] of Object.entries(object)) {
if (JsonLd.forbiddenDirectives.has(key)) {
throw new JsonLdForbiddenDirectiveError(key);
}
if (typeof value === 'object' && value !== null) {
this.checkForForbiddenDirectives(value);
}
}
}
}
}
@bindThis
private getLoader() {
return async (url: string): Promise<RemoteDocument> => {
@@ -122,13 +184,27 @@ class JsonLd {
}
}
const cached = this.cache.get(url);
if (cached) {
if (this.debug) console.debug(`HIT: ${url}`);
return cached;
}
if (this.debug) console.debug(`MISS: ${url}`);
if (this.frozen) throw new JsonLdCacheFrozenError();
const document = await this.fetchDocument(url);
return {
this.checkForForbiddenDirectives(document);
const remoteDocument = {
contextUrl: undefined,
document: document,
documentUrl: url,
};
this.cache.set(url, remoteDocument);
if (this.cache.size > 256) throw new JsonLdCacheOverflowError();
return remoteDocument;
};
}

View File

@@ -21,7 +21,7 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { JsonLdError, JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -163,22 +163,17 @@ export class InboxProcessorService implements OnApplicationShutdown {
const jsonLd = this.jsonLdService.use();
// LD-Signature検証
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
if (!verified) {
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
}
// アクティビティを正規化
delete activity.signature;
try {
activity = await jsonLd.compact(activity) as IActivity;
} catch (e) {
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
} catch (error) {
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${error}`);
}
try {
jsonLd.checkForForbiddenDirectives(activity);
} catch (error) {
throw new Bull.UnrecoverableError(`skip: ${error}`);
}
// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
activity.signature = ldSignature;
//#region Log
const compactedInfo = Object.assign({}, activity);
@@ -186,6 +181,25 @@ export class InboxProcessorService implements OnApplicationShutdown {
this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
//#endregion
activity.signature = ldSignature;
jsonLd.freeze();
// LD-Signature検証
let verified;
try {
verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem);
if (!verified) {
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
}
} catch (error) {
if (error instanceof JsonLdError) {
throw new Bull.UnrecoverableError(`skip: encountered a JSON-LD error while verifying signature: ${error}`);
} else {
throw error;
}
}
// もう一度actorチェック
if (authUser.user.uri !== getApId(activity.actor)) {
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`);

View File

@@ -54,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRoom);
}
if (!await this.chatService.hasPermissionToViewRoomInfo(me.id, room)) {
throw new ApiError(meta.errors.noSuchRoom);
}
return this.chatEntityService.packRoom(room, me);
});
}

View File

@@ -58,7 +58,7 @@ export class LocaleInliner {
collectsModifications() {
for (const chunk of this.chunks) {
if (!chunk.sourceCode) {
if (chunk.sourceCode == null) {
throw new Error(`Source code for ${chunk.fileName} is not loaded.`);
}
const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
@@ -80,7 +80,7 @@ export class LocaleInliner {
await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true });
const localeLogger = localeName === 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only
for (const chunk of this.chunks) {
if (!chunk.sourceCode || !chunk.modifications) {
if (chunk.sourceCode == null || !chunk.modifications) {
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
}
const fileLogger = localeLogger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);

View File

@@ -18,6 +18,7 @@ interface WalkerContext {
}
export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] {
if (sourceCode === '') return [];
let programNode: RolldownESTree.Program;
try {
programNode = parseAst(sourceCode);

View File

@@ -29,6 +29,8 @@ export type Theme = {
export type CompiledTheme = Record<string, string>;
const MAX_THEME_REFERENCE_DEPTH = 8;
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const getBuiltinThemes = () => Promise.all(
@@ -56,45 +58,68 @@ export const getBuiltinThemes = () => Promise.all(
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
export function compile(theme: Theme): CompiledTheme {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
function getThemeReferenceColor(theme: Theme, key: string, stack: string[], depth: number): tinycolor.Instance {
if (depth >= MAX_THEME_REFERENCE_DEPTH) {
throw new Error('Theme reference limit exceeded');
}
if (stack.includes(key)) {
throw new Error('Theme contains circular references');
}
const nextValue = theme.props[key];
if (typeof nextValue !== 'string') {
throw new Error(`Theme references missing property: ${key}`);
}
return getColor(theme, nextValue, [...stack, key], depth + 1);
}
function getColor(theme: Theme, val: string, stack: string[] = [], depth = 0): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getThemeReferenceColor(theme, val.substring(1), stack, depth);
} else if (val[0] === '$') { // ref (const)
return getThemeReferenceColor(theme, val, stack, depth);
} else if (val[0] === ':') { // func
if (depth >= MAX_THEME_REFERENCE_DEPTH) {
throw new Error('Theme reference limit exceeded');
}
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(theme, parts.join('<'), stack, depth + 1);
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
export function compile(theme: Theme): CompiledTheme {
const props = {} as CompiledTheme;
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(theme, v));
}
return props;
return Object.fromEntries(
Object.entries(props).filter(([key]) => themeProps.includes(key)),
) as CompiledTheme;
}
function genValue(c: tinycolor.Instance): string {

View File

@@ -138,6 +138,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, useTemplateRef, computed } from 'vue';
import { host, version } from '@@/js/config.js';
import { DEFAULT_EMOJIS } from '@@/js/const.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
@@ -150,7 +151,6 @@ import { definePage } from '@/page.js';
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
import { $i } from '@/i.js';
import { prefer } from '@/preferences.js';
import { DEFAULT_EMOJIS } from '@@/js/const.js';
const patronsWithIcon = [{
name: 'カイヤン',
@@ -299,6 +299,9 @@ const patronsWithIcon = [{
}, {
name: '大賀愛一郎',
icon: 'https://assets.misskey-hub.net/patrons/c701a797d1df4125970f25d3052250ac.jpg',
}, {
name: '西野マチ',
icon: 'https://assets.misskey-hub.net/patrons/962ff1d2f3d040ed8973b62bbff84391.jpg',
}];
const patrons = [
@@ -414,6 +417,7 @@ const patrons = [
'ほとラズ',
'スズカケン',
'蒼井よみこ',
'忍猫',
];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.5.2-beta.1",
"version": "2026.5.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

25
pnpm-lock.yaml generated
View File

@@ -391,8 +391,8 @@ importers:
specifier: 3.6.7
version: 3.6.7
ws:
specifier: 8.20.0
version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
specifier: 8.20.1
version: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)
xev:
specifier: 3.0.2
version: 3.0.2
@@ -4839,7 +4839,7 @@ packages:
engines: {node: '>= 14'}
aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67:
resolution: {gitHosted: true, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
resolution: {gitHosted: true, integrity: sha512-S4eSTHasZz29AMlnU2/zdGP8zikiDiYfYW9kNooAfwVo8OghXdxKuTDDKDAjWsbBxa1+P4uQHa4BNk9MY74rJQ==, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
version: 0.1.16
engines: {vscode: ^1.83.0}
@@ -9325,7 +9325,7 @@ packages:
engines: {node: '>= 0.4'}
storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640:
resolution: {gitHosted: true, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
resolution: {gitHosted: true, integrity: sha512-QaH1uZSlApQ2CZPkHfhmNm89I92L02s3MdbUPG66TmAyqMaqzxd/AvobORBjtTZ0ymUSa3ii482dRXi+fFb19w==, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
version: 0.0.0
peerDependencies:
'@storybook/blocks': ^7.0.0-rc.4
@@ -10353,6 +10353,18 @@ packages:
utf-8-validate:
optional: true
ws@8.20.1:
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
@@ -20504,6 +20516,11 @@ snapshots:
bufferutil: 4.1.0
utf-8-validate: 6.0.6
ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6):
optionalDependencies:
bufferutil: 4.1.0
utf-8-validate: 6.0.6
wsl-utils@0.1.0:
dependencies:
is-wsl: 3.1.1

View File

@@ -58,6 +58,8 @@ minimumReleaseAgeExclude:
- systeminformation # 脆弱性対応。そのうち消す
- sanitize-html # 脆弱性対応。そのうち消す
- launder # 脆弱性対応。そのうち消す
# Renovate security update: ws@8.20.1
- ws@8.20.1
overrides:
'@aiscript-dev/aiscript-languageserver': '-'
chokidar: 5.0.0

View File

@@ -3,6 +3,9 @@
extends: [
'config:recommended',
],
toolSettings: {
nodeMaxMemory: 4096,
},
timezone: 'Asia/Tokyo',
schedule: [
'* 0 * * *',