diff --git a/locales/en-US.yml b/locales/en-US.yml index 1fc6fea3a..5465a3aad 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2243,6 +2243,7 @@ _permissions: "read:clip-favorite": "View favorited clips" "read:federation": "Get federation data" "write:report-abuse": "Report violation" + "write:push-notification": "Receive push notifications" _auth: shareAccessTitle: "Granting application permissions" shareAccess: "Would you like to authorize \"{name}\" to access this account?" diff --git a/locales/index.js b/locales/index.js index 0770fd82a..1eeb2eaf3 100644 --- a/locales/index.js +++ b/locales/index.js @@ -72,7 +72,7 @@ export function build() { .reduce((a, [k, v]) => (a[k] = (() => { const [lang] = k.split('-'); switch (k) { - case 'ja-JP': return v; + case 'ja-JP': return merge(locales['en-US'], v); case 'ja-KS': case 'en-US': return merge(locales['ja-JP'], v); default: return merge( diff --git a/packages/backend/migration/1736599563231-MastodonApp.js b/packages/backend/migration/1736599563231-MastodonApp.js new file mode 100644 index 000000000..db0ccbe83 --- /dev/null +++ b/packages/backend/migration/1736599563231-MastodonApp.js @@ -0,0 +1,18 @@ +export class MastodonOauth1736599563231 { + name = 'MastodonOauth1736599563231' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "app" ADD "website" character varying(128)`); + await queryRunner.query(`COMMENT ON COLUMN "app"."website" IS 'Application website.'`); + await queryRunner.query(`ALTER TABLE "app" ADD "mastodonScopes" character varying(64) array`); + await queryRunner.query(`COMMENT ON COLUMN "app"."mastodonScopes" IS 'Mastodon app scopes, only set for apps created with Mastodon API.'`); + await queryRunner.query(`ALTER TABLE "app" ADD "redirectUris" character varying(512) array DEFAULT '{}' NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "app"."redirectUris" IS 'Redirect URIs.'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "website"`); + await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "mastodonScopes"`); + await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "redirectUris"`); + } +} diff --git a/packages/backend/src/misc/mastodon/mastodon-to-misskey-scopes.ts b/packages/backend/src/misc/mastodon/mastodon-to-misskey-scopes.ts new file mode 100644 index 000000000..046fc8738 --- /dev/null +++ b/packages/backend/src/misc/mastodon/mastodon-to-misskey-scopes.ts @@ -0,0 +1,42 @@ +import { permissions } from 'misskey-js'; + +const mastodonToMisskeyScopes: Map = new Map([ + ['profile', ['read:account']], + ['follow', ['read:following', 'write:following', 'read:blocks', 'write:blocks', 'read:mutes', 'write:mutes']], + ['push', ['write:push-notification']], + ['read:accounts', ['read:account']], + ['read:blocks', ['read:blocks']], + ['read:bookmarks', ['read:favorites']], + ['read:favourites', ['read:reactions']], + ['read:filters', ['read:account']], + ['read:follows', ['read:following']], + ['read:lists', ['read:account']], + ['read:mutes', ['read:mutes']], + ['read:notifications', ['read:notifications']], + ['read:search', []], + ['read:statuses', []], + ['write:accounts', ['write:account']], + ['write:blocks', ['write:blocks']], + ['write:bookmarks', ['write:favorites']], + ['write:conversations', ['write:notes']], + ['write:favourites', ['write:reactions']], + ['write:filters', ['write:account']], + ['write:follows', ['write:following']], + ['write:lists', ['write:account']], + ['write:media', ['read:drive', 'write:drive']], + ['write:mutes', ['write:mutes']], + ['write:notifications', ['write:notifications']], + ['write:reports', ['write:report-abuse']], + ['write:statuses', ['write:notes']], +]); + +function setHighLevelScope(scopeName: string) { + const granularScopes = Array.from(mastodonToMisskeyScopes) + .flatMap(([key, value]) => key.startsWith(scopeName + ':') ? value : []); + mastodonToMisskeyScopes.set(scopeName, Array.from(new Set(granularScopes))); +} + +setHighLevelScope('read'); +setHighLevelScope('write'); + +export { mastodonToMisskeyScopes }; diff --git a/packages/backend/src/models/App.ts b/packages/backend/src/models/App.ts index fc61c60bb..176b49ca2 100644 --- a/packages/backend/src/models/App.ts +++ b/packages/backend/src/models/App.ts @@ -63,4 +63,22 @@ export class MiApp { comment: 'The callbackUrl of the App.', }) public callbackUrl: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'Application website.', + }) + public website: string; + + @Column('varchar', { + length: 64, array: true, nullable: true, + comment: 'Mastodon app scopes, only set for apps created with Mastodon API.', + }) + public mastodonScopes: string[]; + + @Column('varchar', { + length: 512, array: true, nullable: true, + comment: 'Redirect URIs.', + }) + public redirectUris: string[]; } diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 60b5c396f..cd5f3a881 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; +import fastifyFormbody from '@fastify/formbody'; import { ModuleRef } from '@nestjs/core'; import type { Config } from '@/config.js'; import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; @@ -63,6 +64,13 @@ export class ApiServerService { done(); }); + fastify.register(this.createMisskeyServer); + fastify.register(this.createMastodonServer); + done(); + } + + @bindThis + private createMisskeyServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { for (const endpoint of endpoints) { const ep = { name: endpoint.name, @@ -106,32 +114,6 @@ export class ApiServerService { } } - const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({ - name: endpoint.name, - method: endpoint.method, - meta: endpoint.meta, - params: endpoint.params, - exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec, - }); - const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name)) - .map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) })); - for (const { name, endpoints } of groupedMastodonEndpoints) { - fastify.all<{ - Params: { endpoint: string; }, - Body: Record, - Querystring: Record, - }>('/' + name, async (request, reply) => { - const ep = endpoints.get(request.method); - if (!ep) { - reply.code(405); - reply.send(); - return; - } - await this.apiCallService.handleMastodonRequest(ep, request, reply); - return reply; - }); - } - fastify.post<{ Body: { username: string; @@ -212,4 +194,39 @@ export class ApiServerService { done(); } + + @bindThis + private createMastodonServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + fastify.register(fastifyFormbody); + + const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({ + name: endpoint.name, + method: endpoint.method, + meta: endpoint.meta, + params: endpoint.params, + exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec, + }); + + const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name)) + .map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) })); + + for (const { name, endpoints } of groupedMastodonEndpoints) { + fastify.all<{ + Params: { endpoint: string; }, + Body: Record, + Querystring: Record, + }>('/' + name, async (request, reply) => { + const ep = endpoints.get(request.method); + if (!ep) { + reply.code(405); + reply.send(); + return; + } + await this.apiCallService.handleMastodonRequest(ep, request, reply); + return reply; + }); + } + + done(); + } } diff --git a/packages/backend/src/server/api/MastodonEndpointsModule.ts b/packages/backend/src/server/api/MastodonEndpointsModule.ts index 099da28af..4570f66d6 100644 --- a/packages/backend/src/server/api/MastodonEndpointsModule.ts +++ b/packages/backend/src/server/api/MastodonEndpointsModule.ts @@ -2,8 +2,10 @@ import { Module, Provider } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js'; +import * as mep___apps_v1_post from './mastodon/apps/v1/post.js'; const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default }; +const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___apps_v1_post.default }; @Module({ imports: [ @@ -11,9 +13,11 @@ const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup ], providers: [ $accounts_lookup_v1_get, + $apps_v1_post, ], exports: [ $accounts_lookup_v1_get, + $apps_v1_post, ], }) export class MastodonEndpointsModule {} diff --git a/packages/backend/src/server/api/mastodon-endpoints.ts b/packages/backend/src/server/api/mastodon-endpoints.ts index 6c138c206..62356e61d 100644 --- a/packages/backend/src/server/api/mastodon-endpoints.ts +++ b/packages/backend/src/server/api/mastodon-endpoints.ts @@ -1,8 +1,10 @@ import { Schema } from '@/misc/json-schema.js'; import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js'; +import * as mep___apps_v1_post from './mastodon/apps/v1/post.js'; const eps = [ ['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get], + ['POST', 'v1/apps', mep___apps_v1_post], ]; export interface IMastodonEndpointMeta { diff --git a/packages/backend/src/server/api/mastodon/apps/v1/post.ts b/packages/backend/src/server/api/mastodon/apps/v1/post.ts new file mode 100644 index 000000000..2e53bb124 --- /dev/null +++ b/packages/backend/src/server/api/mastodon/apps/v1/post.ts @@ -0,0 +1,80 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MastodonEndpoint } from '@/server/api/mastodon-endpoint-base.js'; +import { MastodonApiError } from '@/server/api/error.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import type { AppsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { IdService } from '@/core/IdService.js'; +import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js'; + +export const meta = { + requireCredential: false, +} as const; + +export const paramDef = { + type: 'object', + properties: { + client_name: { type: 'string', minLength: 1 }, + redirect_uri: { type: 'string', minLength: 1 }, + redirect_uris: { + anyOf: [ + { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } }, + { type: 'string', minLength: 1 }, + ], + }, + scopes: { type: 'string', minLength: 1 }, + website: { type: 'string' }, + }, + anyOf: [ + { required: ['redirect_uri'] }, + { required: ['redirect_uris'] }, + ], + required: ['client_name'], +} as const; + +@Injectable() +export default class extends MastodonEndpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const redirectUrlsRawValue = ps.redirect_uris ?? ps.redirect_uri ?? []; + const redirectUris = typeof redirectUrlsRawValue === 'string' ? redirectUrlsRawValue.split('\n') : redirectUrlsRawValue; + const secret = secureRndstr(32); + const mastodonScopes = (ps.scopes ?? 'read').split(' '); + const scopes = mastodonScopes.flatMap(scope => { + const misskeyScopes = mastodonToMisskeyScopes.get(scope); + if (!misskeyScopes) { + throw new MastodonApiError('Scopes doesn\'t match configured on the server.', 400); + } + return misskeyScopes; + }); + const clientId = this.idService.gen(); + await this.appsRepository.insert({ + id: clientId, + userId: me ? me.id : null, + name: ps.client_name, + description: ps.website ?? '', + permission: scopes, + callbackUrl: null, + secret: secret, + website: ps.website, + mastodonScopes: mastodonScopes, + redirectUris: redirectUris, + }); + return { + id: clientId, + name: ps.client_name, + website: ps.website, + scopes: mastodonScopes, + redirect_uri: redirectUris.join('\n'), + redirect_uris: redirectUris, + client_id: `mastodon:${clientId}`, + client_secret: secret, + }; + }); + } +} diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index beaabc610..fea3a1042 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -34,6 +34,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { AccessTokensRepository, + AppsRepository, IndieAuthClientsRepository, UserProfilesRepository, UsersRepository, @@ -45,9 +46,15 @@ import { LoggerService } from '@/core/LoggerService.js'; import Logger from '@/logger.js'; import { StatusError } from '@/misc/status-error.js'; import { normalizeEmailAddress } from '@/misc/normalize-email-address.js'; +import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js'; import type { ServerResponse } from 'node:http'; import type { FastifyInstance } from 'fastify'; +function extractMastodonAppId(clientId: string): string | null { + const MASTODON_CLIENT_ID_PREFIX = 'mastodon:'; + return clientId.startsWith(MASTODON_CLIENT_ID_PREFIX) ? clientId.substring(MASTODON_CLIENT_ID_PREFIX.length) : null; +} + // TODO: Consider migrating to @node-oauth/oauth2-server once // https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. // Upstream the various validations and RFC9207 implementation in that case. @@ -258,6 +265,8 @@ export class OAuth2ProviderService { private usersRepository: UsersRepository, @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, private idService: IdService, private cacheService: CacheService, @@ -328,13 +337,25 @@ export class OAuth2ProviderService { if (body.client_id !== granted.clientId) return; if (redirectUri !== granted.redirectUri) return; + const mastodonAppId = extractMastodonAppId(granted.clientId); + // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6 - if (!body.code_verifier) return; - if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return; + // For Mastodon API, code verifier isn't necessary (but if code challenge was provided, then it should be verified) + if (!mastodonAppId || granted.codeChallenge || body.code_verifier) { + if (!body.code_verifier) return; + if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return; + } const accessToken = secureRndstr(128); const now = new Date(); + let scopes = granted.scopes; + let name = granted.clientId; + if (mastodonAppId) { + scopes = [...new Set(granted.scopes.flatMap((scope: string) => mastodonToMisskeyScopes.get(scope)))]; + name = (await this.appsRepository.findOneBy({ id: mastodonAppId }))?.name ?? name; + } + // NOTE: we don't have a setup for automatic token expiration await accessTokensRepository.insert({ id: idService.gen(now.getTime()), @@ -342,8 +363,9 @@ export class OAuth2ProviderService { userId: granted.userId, token: accessToken, hash: accessToken, - name: granted.clientId, - permission: granted.scopes, + name: name, + permission: scopes, + appId: mastodonAppId, }); grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`); @@ -354,7 +376,7 @@ export class OAuth2ProviderService { } granted.grantedToken = accessToken; - this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`); + this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${scopes}]`); await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5); return [accessToken, undefined, { scope: granted.scopes.join(' ') }]; @@ -418,50 +440,75 @@ export class OAuth2ProviderService { this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`); - const clientUrl = validateClientId(clientID); + let clientInfo: ClientInformation; + const mastodonAppId = extractMastodonAppId(clientID); + if (mastodonAppId) { + const app = await this.appsRepository.findOneBy({ id: mastodonAppId }); + if (!app) { + throw new AuthorizationError('unrecognized client id', 'invalid_request'); + } + clientInfo = { + id: clientID, + name: app.name, + redirectUris: app.redirectUris, + }; + if (codeChallengeMethod && codeChallengeMethod !== 'S256') { + throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request'); + } + try { + const scopes = [...new Set(scope)].filter(s => mastodonToMisskeyScopes.has(s)); + if (!scopes.length) { + throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); + } + areq.scope = scopes; + } catch (err) { + return [err as Error, clientInfo, redirectURI]; + } + } else { + const clientUrl = validateClientId(clientID); - // https://indieauth.spec.indieweb.org/#client-information-discovery - // "the server may want to resolve the domain name first and avoid fetching the document - // if the IP address is within the loopback range defined by [RFC5735] - // or any other implementation-specific internal IP address." - if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') { - const lookup = await dns.lookup(clientUrl.hostname); - if (ipaddr.parse(lookup.address).range() !== 'unicast') { - throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request'); + // https://indieauth.spec.indieweb.org/#client-information-discovery + // "the server may want to resolve the domain name first and avoid fetching the document + // if the IP address is within the loopback range defined by [RFC5735] + // or any other implementation-specific internal IP address." + if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') { + const lookup = await dns.lookup(clientUrl.hostname); + if (ipaddr.parse(lookup.address).range() !== 'unicast') { + throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request'); + } + } + + // Find client information from the database. + const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null; + // Find client information from the remote. + clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href); + + try { + const scopes = [...new Set(scope)].filter(s => (kinds).includes(s)); + if (!scopes.length) { + throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); + } + areq.scope = scopes; + + // Require PKCE parameters. This requirement is skipped for Mastodon clients, as Mastodon API doesn't require it. + // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack: + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack + if (typeof codeChallenge !== 'string') { + throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request'); + } + if (codeChallengeMethod !== 'S256') { + throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request'); + } + } catch (err) { + return [err as Error, clientInfo, redirectURI]; } } - - // Find client information from the database. - const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null; - // Find client information from the remote. - const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href); - // Require the redirect URI to be included in an explicit list, per // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 if (!clientInfo.redirectUris.includes(redirectURI)) { throw new AuthorizationError('Invalid redirect_uri', 'invalid_request'); } - try { - const scopes = [...new Set(scope)].filter(s => (kinds).includes(s)); - if (!scopes.length) { - throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope'); - } - areq.scope = scopes; - - // Require PKCE parameters. - // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack: - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack - if (typeof codeChallenge !== 'string') { - throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request'); - } - if (codeChallengeMethod !== 'S256') { - throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request'); - } - } catch (err) { - return [err as Error, clientInfo, redirectURI]; - } - return [null, clientInfo, redirectURI]; })().then(args => done(...args), err => done(err)); }) as ValidateFunctionArity2)); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 94b056fa3..9f7e34fa2 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2820,7 +2820,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content'] function parse(acct: string): Acct; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:push-notification"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 470490c15..f3c7dfb79 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -103,6 +103,7 @@ export const permissions = [ 'read:clip-favorite', 'read:federation', 'write:report-abuse', + 'write:push-notification', // Mastodon permission ] as const; export const moderationLogTypes = [