feat(backend): support OAuth 2.0 authorization
This commit is contained in:
parent
1b1f82a2e2
commit
4d9f78f20f
|
@ -61,6 +61,7 @@
|
||||||
"@fastify/accepts": "4.2.0",
|
"@fastify/accepts": "4.2.0",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.3.0",
|
"@fastify/cors": "8.3.0",
|
||||||
|
"@fastify/express": "^2.3.0",
|
||||||
"@fastify/http-proxy": "9.2.1",
|
"@fastify/http-proxy": "9.2.1",
|
||||||
"@fastify/multipart": "7.7.0",
|
"@fastify/multipart": "7.7.0",
|
||||||
"@fastify/static": "6.10.2",
|
"@fastify/static": "6.10.2",
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
"bullmq": "4.1.0",
|
"bullmq": "4.1.0",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.0",
|
"cbor": "9.0.0",
|
||||||
|
@ -98,6 +100,7 @@
|
||||||
"got": "13.0.0",
|
"got": "13.0.0",
|
||||||
"happy-dom": "9.20.3",
|
"happy-dom": "9.20.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
|
"http-link-header": "^1.1.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "3.1.0",
|
||||||
"ipaddr.js": "2.1.0",
|
"ipaddr.js": "2.1.0",
|
||||||
|
@ -117,10 +120,13 @@
|
||||||
"nodemailer": "6.9.3",
|
"nodemailer": "6.9.3",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
|
"oauth2orize": "^1.11.1",
|
||||||
|
"oauth2orize-pkce": "^0.1.2",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "9.1.2",
|
"otpauth": "9.1.2",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.11.0",
|
"pg": "8.11.0",
|
||||||
|
"pkce-challenge": "^4.0.1",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
|
@ -166,11 +172,13 @@
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.2",
|
"@types/archiver": "5.3.2",
|
||||||
"@types/bcryptjs": "2.4.2",
|
"@types/bcryptjs": "2.4.2",
|
||||||
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
"@types/color-convert": "2.0.0",
|
"@types/color-convert": "2.0.0",
|
||||||
"@types/content-disposition": "0.5.5",
|
"@types/content-disposition": "0.5.5",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.21",
|
"@types/fluent-ffmpeg": "2.1.21",
|
||||||
|
"@types/http-link-header": "^1.0.3",
|
||||||
"@types/jest": "29.5.2",
|
"@types/jest": "29.5.2",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.1",
|
"@types/jsdom": "21.1.1",
|
||||||
|
@ -182,6 +190,7 @@
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.8",
|
"@types/nodemailer": "6.4.8",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
|
"@types/oauth2orize": "^1.11.0",
|
||||||
"@types/pg": "8.10.2",
|
"@types/pg": "8.10.2",
|
||||||
"@types/pug": "2.0.6",
|
"@types/pug": "2.0.6",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
|
@ -193,6 +202,7 @@
|
||||||
"@types/sanitize-html": "2.9.0",
|
"@types/sanitize-html": "2.9.0",
|
||||||
"@types/semver": "7.5.0",
|
"@types/semver": "7.5.0",
|
||||||
"@types/sharp": "0.32.0",
|
"@types/sharp": "0.32.0",
|
||||||
|
"@types/simple-oauth2": "^5.0.4",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/tmp": "0.2.3",
|
"@types/tmp": "0.2.3",
|
||||||
|
@ -210,6 +220,7 @@
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
"jest-mock": "29.5.0"
|
"jest-mock": "29.5.0",
|
||||||
|
"simple-oauth2": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module 'oauth2orize-pkce' {
|
||||||
|
export default {
|
||||||
|
extensions(): any;
|
||||||
|
};
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||||
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -78,6 +79,7 @@ import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.
|
||||||
ServerStatsChannelService,
|
ServerStatsChannelService,
|
||||||
UserListChannelService,
|
UserListChannelService,
|
||||||
OpenApiServerService,
|
OpenApiServerService,
|
||||||
|
OAuth2ProviderService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ServerService,
|
ServerService,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
import { FileServerService } from './FileServerService.js';
|
import { FileServerService } from './FileServerService.js';
|
||||||
import { ClientServerService } from './web/ClientServerService.js';
|
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';
|
||||||
|
|
||||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
@ -56,12 +57,13 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
private clientServerService: ClientServerService,
|
private clientServerService: ClientServerService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private oauth2ProviderService: OAuth2ProviderService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
this.logger = this.loggerService.getLogger('server', 'gray', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async launch() {
|
public async launch(): Promise<void> {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
trustProxy: true,
|
trustProxy: true,
|
||||||
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
|
logger: !['production', 'test'].includes(process.env.NODE_ENV ?? ''),
|
||||||
|
@ -90,6 +92,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
fastify.register(this.activityPubServerService.createServer);
|
fastify.register(this.activityPubServerService.createServer);
|
||||||
fastify.register(this.nodeinfoServerService.createServer);
|
fastify.register(this.nodeinfoServerService.createServer);
|
||||||
fastify.register(this.wellKnownServerService.createServer);
|
fastify.register(this.wellKnownServerService.createServer);
|
||||||
|
fastify.register(this.oauth2ProviderService.createServer);
|
||||||
|
|
||||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||||
const path = request.params.path;
|
const path = request.params.path;
|
||||||
|
|
|
@ -0,0 +1,466 @@
|
||||||
|
import dns from 'node:dns/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import httpLinkHeader from 'http-link-header';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
|
import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
|
||||||
|
import oauth2Pkce from 'oauth2orize-pkce';
|
||||||
|
import fastifyView from '@fastify/view';
|
||||||
|
import pug from 'pug';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import fastifyExpress from '@fastify/express';
|
||||||
|
import { verifyChallenge } from 'pkce-challenge';
|
||||||
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import { kinds } from '@/misc/api-permissions.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { AccessTokensRepository, UsersRepository } from '@/models/index.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
|
import { MemoryKVCache } from '@/misc/cache.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
|
import type { ServerResponse } from 'node:http';
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Follows https://indieauth.spec.indieweb.org/#client-identifier
|
||||||
|
// This is also mostly similar to https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation
|
||||||
|
// although Google has stricter rule.
|
||||||
|
function validateClientId(raw: string): URL {
|
||||||
|
// "Clients are identified by a [URL]."
|
||||||
|
const url = ((): URL => {
|
||||||
|
try {
|
||||||
|
return new URL(raw);
|
||||||
|
} catch { throw new AuthorizationError('client_id must be a valid URL', 'invalid_request'); }
|
||||||
|
})();
|
||||||
|
|
||||||
|
// "Client identifier URLs MUST have either an https or http scheme"
|
||||||
|
// But then again:
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.1
|
||||||
|
// 'The redirection endpoint SHOULD require the use of TLS as described
|
||||||
|
// in Section 1.6 when the requested response type is "code" or "token"'
|
||||||
|
// TODO: Consider allowing custom URIs per RFC 8252.
|
||||||
|
const allowedProtocols = process.env.NODE_ENV === 'test' ? ['http:', 'https:'] : ['https:'];
|
||||||
|
if (!allowedProtocols.includes(url.protocol)) {
|
||||||
|
throw new AuthorizationError('client_id must be a valid HTTPS URL', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// "MUST contain a path component (new URL() implicitly adds one)"
|
||||||
|
|
||||||
|
// "MUST NOT contain single-dot or double-dot path segments,"
|
||||||
|
const segments = url.pathname.split('/');
|
||||||
|
if (segments.includes('.') || segments.includes('..')) {
|
||||||
|
throw new AuthorizationError('client_id must not contain dot path segments', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ("MAY contain a query string component")
|
||||||
|
|
||||||
|
// "MUST NOT contain a fragment component"
|
||||||
|
if (url.hash) {
|
||||||
|
throw new AuthorizationError('client_id must not contain a fragment component', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// "MUST NOT contain a username or password component"
|
||||||
|
if (url.username || url.password) {
|
||||||
|
throw new AuthorizationError('client_id must not contain a username or a password', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ("MAY contain a port")
|
||||||
|
|
||||||
|
// "host names MUST be domain names or a loopback interface and MUST NOT be
|
||||||
|
// IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1]."
|
||||||
|
if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
|
||||||
|
throw new AuthorizationError('client_id must have a domain name as a host name', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientInformation {
|
||||||
|
id: string;
|
||||||
|
redirectUris: string[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||||
|
// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
|
||||||
|
// and if there is an [h-app] with a url property matching the client_id URL,
|
||||||
|
// then it should use the name and icon and display them on the authorization prompt."
|
||||||
|
// (But we don't display any icon for now)
|
||||||
|
// https://indieauth.spec.indieweb.org/#redirect-url
|
||||||
|
// "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
|
||||||
|
// of redirect_uri at the client_id URL.
|
||||||
|
// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
|
||||||
|
// look for an exact match of the given redirect_uri in the request against the list of
|
||||||
|
// redirect_uris discovered after resolving any relative URLs."
|
||||||
|
async function discoverClientInformation(httpRequestService: HttpRequestService, id: string): Promise<ClientInformation> {
|
||||||
|
try {
|
||||||
|
const res = await httpRequestService.send(id);
|
||||||
|
const redirectUris: string[] = [];
|
||||||
|
|
||||||
|
const linkHeader = res.headers.get('link');
|
||||||
|
if (linkHeader) {
|
||||||
|
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = JSDOM.fragment(await res.text());
|
||||||
|
|
||||||
|
redirectUris.push(...[...fragment.querySelectorAll<HTMLLinkElement>('link[rel=redirect_uri][href]')].map(el => el.href));
|
||||||
|
|
||||||
|
const name = fragment.querySelector<HTMLElement>('.h-app .p-name')?.textContent?.trim() ?? id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
throw new AuthorizationError('Failed to fetch client information', 'server_error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OmitFirstElement<T extends unknown[]> = T extends [unknown, ...(infer R)]
|
||||||
|
? R
|
||||||
|
: [];
|
||||||
|
|
||||||
|
interface OAuthParsedRequest extends OAuth2Req {
|
||||||
|
codeChallenge: string;
|
||||||
|
codeChallengeMethod: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OAuthHttpResponse extends ServerResponse {
|
||||||
|
redirect(location: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OAuth2DecisionRequest extends MiddlewareRequest {
|
||||||
|
body: {
|
||||||
|
transaction_id: string;
|
||||||
|
cancel: boolean;
|
||||||
|
login_token: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryMode(issuerUrl: string): oauth2orize.grant.Options['modes'] {
|
||||||
|
return {
|
||||||
|
query: (txn, res, params): void => {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
|
||||||
|
// "In authorization responses to the client, including error responses,
|
||||||
|
// an authorization server supporting this specification MUST indicate its
|
||||||
|
// identity by including the iss parameter in the response."
|
||||||
|
params.iss = issuerUrl;
|
||||||
|
|
||||||
|
const parsed = new URL(txn.redirectURI);
|
||||||
|
for (const [key, value] of Object.entries(params)) {
|
||||||
|
parsed.searchParams.append(key, value as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (res as OAuthHttpResponse).redirect(parsed.toString());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the transaction ID and the oauth/authorize parameters.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. oauth/authorize endpoint will call store() to store the parameters
|
||||||
|
* and puts the generated transaction ID to the dialog page
|
||||||
|
* 2. oauth/decision will call load() to retrieve the parameters and then remove()
|
||||||
|
*/
|
||||||
|
class OAuth2Store {
|
||||||
|
#cache = new MemoryKVCache<OAuth2>(1000 * 60 * 5); // expires after 5min
|
||||||
|
|
||||||
|
load(req: OAuth2DecisionRequest, cb: (err: Error | null, txn?: OAuth2) => void): void {
|
||||||
|
const { transaction_id } = req.body;
|
||||||
|
if (!transaction_id) {
|
||||||
|
cb(new AuthorizationError('Missing transaction ID', 'invalid_request'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loaded = this.#cache.get(transaction_id);
|
||||||
|
if (!loaded) {
|
||||||
|
cb(new AuthorizationError('Invalid or expired transaction ID', 'access_denied'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb(null, loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
store(req: OAuth2DecisionRequest, oauth2: OAuth2, cb: (err: Error | null, transactionID?: string) => void): void {
|
||||||
|
const transactionId = secureRndstr(128, true);
|
||||||
|
this.#cache.set(transactionId, oauth2);
|
||||||
|
cb(null, transactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(req: OAuth2DecisionRequest, tid: string, cb: () => void): void {
|
||||||
|
this.#cache.delete(tid);
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OAuth2ProviderService {
|
||||||
|
#server = oauth2orize.createServer({
|
||||||
|
store: new OAuth2Store(),
|
||||||
|
});
|
||||||
|
#logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
|
private httpRequestService: HttpRequestService,
|
||||||
|
@Inject(DI.accessTokensRepository)
|
||||||
|
accessTokensRepository: AccessTokensRepository,
|
||||||
|
idService: IdService,
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
private cacheService: CacheService,
|
||||||
|
loggerService: LoggerService,
|
||||||
|
) {
|
||||||
|
this.#logger = loggerService.getLogger('oauth');
|
||||||
|
|
||||||
|
const grantCodeCache = new MemoryKVCache<{
|
||||||
|
clientId: string,
|
||||||
|
userId: string,
|
||||||
|
redirectUri: string,
|
||||||
|
codeChallenge: string,
|
||||||
|
scopes: string[],
|
||||||
|
|
||||||
|
// fields to prevent multiple code use
|
||||||
|
grantedToken?: string,
|
||||||
|
revoked?: boolean,
|
||||||
|
used?: boolean,
|
||||||
|
}>(1000 * 60 * 5); // expires after 5m
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
|
||||||
|
// "Authorization servers MUST support PKCE [RFC7636]."
|
||||||
|
this.#server.grant(oauth2Pkce.extensions());
|
||||||
|
this.#server.grant(oauth2orize.grant.code({
|
||||||
|
modes: getQueryMode(config.url),
|
||||||
|
}, (client, redirectUri, token, ares, areq, locals, done) => {
|
||||||
|
(async (): Promise<OmitFirstElement<Parameters<typeof done>>> => {
|
||||||
|
this.#logger.info(`Checking the user before sending authorization code to ${client.id}`);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AuthorizationError('No user', 'invalid_request');
|
||||||
|
}
|
||||||
|
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
|
||||||
|
() => this.usersRepository.findOneBy({ token }) as Promise<LocalUser | null>);
|
||||||
|
if (!user) {
|
||||||
|
throw new AuthorizationError('No such user', 'invalid_request');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${client.id} through ${redirectUri}, with scope: [${areq.scope}]`);
|
||||||
|
|
||||||
|
const code = secureRndstr(128, true);
|
||||||
|
grantCodeCache.set(code, {
|
||||||
|
clientId: client.id,
|
||||||
|
userId: user.id,
|
||||||
|
redirectUri,
|
||||||
|
codeChallenge: (areq as OAuthParsedRequest).codeChallenge,
|
||||||
|
scopes: areq.scope,
|
||||||
|
});
|
||||||
|
return [code];
|
||||||
|
})().then(args => done(null, ...args), err => done(err));
|
||||||
|
}));
|
||||||
|
this.#server.exchange(oauth2orize.exchange.authorizationCode((client, code, redirectUri, body, authInfo, done) => {
|
||||||
|
(async (): Promise<OmitFirstElement<Parameters<typeof done>> | undefined> => {
|
||||||
|
this.#logger.info('Checking the received authorization code for the exchange');
|
||||||
|
const granted = grantCodeCache.get(code);
|
||||||
|
if (!granted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2
|
||||||
|
// "If an authorization code is used more than once, the authorization server
|
||||||
|
// MUST deny the request and SHOULD revoke (when possible) all tokens
|
||||||
|
// previously issued based on that authorization code."
|
||||||
|
if (granted.used) {
|
||||||
|
this.#logger.info(`Detected multiple code use from ${granted.clientId} for user ${granted.userId}. Revoking the code.`);
|
||||||
|
grantCodeCache.delete(code);
|
||||||
|
granted.revoked = true;
|
||||||
|
if (granted.grantedToken) {
|
||||||
|
await accessTokensRepository.delete({ token: granted.grantedToken });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
granted.used = true;
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3
|
||||||
|
if (body.client_id !== granted.clientId) return;
|
||||||
|
if (redirectUri !== granted.redirectUri) return;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const accessToken = secureRndstr(128, true);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// NOTE: we don't have a setup for automatic token expiration
|
||||||
|
await accessTokensRepository.insert({
|
||||||
|
id: idService.genId(),
|
||||||
|
createdAt: now,
|
||||||
|
lastUsedAt: now,
|
||||||
|
userId: granted.userId,
|
||||||
|
token: accessToken,
|
||||||
|
hash: accessToken,
|
||||||
|
name: granted.clientId,
|
||||||
|
permission: granted.scopes,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (granted.revoked) {
|
||||||
|
this.#logger.info('Canceling the token as the authorization code was revoked in parallel during the process.');
|
||||||
|
await accessTokensRepository.delete({ token: accessToken });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
granted.grantedToken = accessToken;
|
||||||
|
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
|
||||||
|
|
||||||
|
return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
|
||||||
|
})().then(args => done(null, ...args ?? []), err => done(err));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async createServer(fastify: FastifyInstance): Promise<void> {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8414.html
|
||||||
|
// https://indieauth.spec.indieweb.org/#indieauth-server-metadata
|
||||||
|
fastify.get('/.well-known/oauth-authorization-server', async (_request, reply) => {
|
||||||
|
reply.send({
|
||||||
|
issuer: this.config.url,
|
||||||
|
authorization_endpoint: new URL('/oauth/authorize', this.config.url),
|
||||||
|
token_endpoint: new URL('/oauth/token', this.config.url),
|
||||||
|
scopes_supported: kinds,
|
||||||
|
response_types_supported: ['code'],
|
||||||
|
grant_types_supported: ['authorization_code'],
|
||||||
|
service_documentation: 'https://misskey-hub.net',
|
||||||
|
code_challenge_methods_supported: ['S256'],
|
||||||
|
authorization_response_iss_parameter_supported: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.get('/oauth/authorize', async (request, reply) => {
|
||||||
|
const oauth2 = (request.raw as MiddlewareRequest).oauth2;
|
||||||
|
if (!oauth2) {
|
||||||
|
throw new Error('Unexpected lack of authorization information');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
|
||||||
|
|
||||||
|
reply.header('Cache-Control', 'no-store');
|
||||||
|
return await reply.view('oauth', {
|
||||||
|
transactionId: oauth2.transactionID,
|
||||||
|
clientName: oauth2.client.name,
|
||||||
|
scope: oauth2.req.scope.join(' '),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fastify.post('/oauth/decision', async () => { });
|
||||||
|
fastify.post('/oauth/token', async () => { });
|
||||||
|
|
||||||
|
fastify.register(fastifyView, {
|
||||||
|
root: fileURLToPath(new URL('../web/views', import.meta.url)),
|
||||||
|
engine: { pug },
|
||||||
|
defaultContext: {
|
||||||
|
version: this.config.version,
|
||||||
|
config: this.config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fastify.register(fastifyExpress);
|
||||||
|
fastify.use('/oauth/authorize', this.#server.authorize(((areq, done) => {
|
||||||
|
(async (): Promise<Parameters<typeof done>> => {
|
||||||
|
// This should return client/redirectURI AND the error, or
|
||||||
|
// the handler can't send error to the redirection URI
|
||||||
|
|
||||||
|
const { codeChallenge, codeChallengeMethod, clientID, redirectURI, scope } = areq as OAuthParsedRequest;
|
||||||
|
|
||||||
|
this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
|
||||||
|
|
||||||
|
const clientUrl = validateClientId(clientID);
|
||||||
|
|
||||||
|
// TODO: Consider allowing localhost for native apps (RFC 8252)
|
||||||
|
// This is currently blocked by the redirect_uri check below, but we can theoretically
|
||||||
|
// loosen the rule for localhost as the data never leaves the client machine.
|
||||||
|
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 remote.
|
||||||
|
const clientInfo = await discoverClientInformation(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));
|
||||||
|
fastify.use('/oauth/authorize', this.#server.errorHandler({
|
||||||
|
mode: 'indirect',
|
||||||
|
modes: getQueryMode(this.config.url),
|
||||||
|
}));
|
||||||
|
fastify.use('/oauth/authorize', this.#server.errorHandler());
|
||||||
|
|
||||||
|
fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false }));
|
||||||
|
fastify.use('/oauth/decision', this.#server.decision((req, done) => {
|
||||||
|
const { body } = req as OAuth2DecisionRequest;
|
||||||
|
this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`);
|
||||||
|
req.user = body.login_token;
|
||||||
|
done(null, undefined);
|
||||||
|
}));
|
||||||
|
fastify.use('/oauth/decision', this.#server.errorHandler());
|
||||||
|
|
||||||
|
// Clients may use JSON or urlencoded
|
||||||
|
fastify.use('/oauth/token', bodyParser.urlencoded({ extended: false }));
|
||||||
|
fastify.use('/oauth/token', bodyParser.json({ strict: true }));
|
||||||
|
fastify.use('/oauth/token', this.#server.token());
|
||||||
|
fastify.use('/oauth/token', this.#server.errorHandler());
|
||||||
|
|
||||||
|
// Return 404 for any unknown paths under /oauth so that clients can know
|
||||||
|
// whether a certain endpoint is supported or not.
|
||||||
|
fastify.all('/oauth/*', async (_request, reply) => {
|
||||||
|
reply.code(404);
|
||||||
|
reply.send({
|
||||||
|
error: {
|
||||||
|
message: 'Unknown OAuth endpoint.',
|
||||||
|
code: 'UNKNOWN_OAUTH_ENDPOINT',
|
||||||
|
id: 'aa49e620-26cb-4e28-aad6-8cbcb58db147',
|
||||||
|
kind: 'client',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
extends ./base
|
||||||
|
|
||||||
|
block meta
|
||||||
|
//- Should be removed by the page when it loads, so that it won't needlessly
|
||||||
|
//- stay when user navigates away via the navigation bar
|
||||||
|
//- XXX: Remove navigation bar in auth page?
|
||||||
|
meta(name='misskey:oauth:transaction-id' content=transactionId)
|
||||||
|
meta(name='misskey:oauth:client-name' content=clientName)
|
||||||
|
meta(name='misskey:oauth:scope' content=scope)
|
|
@ -1,7 +1,7 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream } from '../utils.js';
|
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
|
@ -218,6 +218,42 @@ describe('API', () => {
|
||||||
assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description'));
|
assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: insufficient_scope test (authテストが全然なくて書けない)
|
describe('invalid bearer format', () => {
|
||||||
|
test('No preceding bearer', async () => {
|
||||||
|
const result = await relativeFetch('api/notes/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: alice.token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: 'test' }),
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.status, 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Lowercase bearer', async () => {
|
||||||
|
const result = await relativeFetch('api/notes/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `bearer ${alice.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: 'test' }),
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.status, 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('No space after bearer', async () => {
|
||||||
|
const result = await relativeFetch('api/notes/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer${alice.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: 'test' }),
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.status, 401);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,925 @@
|
||||||
|
/**
|
||||||
|
* Basic OAuth tests to make sure the library is correctly integrated to Misskey
|
||||||
|
* and not regressed by version updates or potential migration to another library.
|
||||||
|
*/
|
||||||
|
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2';
|
||||||
|
import pkceChallenge from 'pkce-challenge';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify';
|
||||||
|
import { api, port, signup, startServer } from '../utils.js';
|
||||||
|
import type * as misskey from 'misskey-js';
|
||||||
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
const host = `http://127.0.0.1:${port}`;
|
||||||
|
|
||||||
|
const clientPort = port + 1;
|
||||||
|
const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`;
|
||||||
|
|
||||||
|
const basicAuthParams: AuthorizationParamsExtended = {
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AuthorizationParamsExtended {
|
||||||
|
redirect_uri: string;
|
||||||
|
scope: string | string[];
|
||||||
|
state: string;
|
||||||
|
code_challenge?: string;
|
||||||
|
code_challenge_method?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizationTokenConfigExtended extends AuthorizationTokenConfig {
|
||||||
|
code_verifier: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetTokenError {
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientConfig: ModuleOptions<'client_id'> = {
|
||||||
|
client: {
|
||||||
|
id: `http://127.0.0.1:${clientPort}/`,
|
||||||
|
secret: '',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
tokenHost: host,
|
||||||
|
tokenPath: '/oauth/token',
|
||||||
|
authorizePath: '/oauth/authorize',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
authorizationMethod: 'body',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } {
|
||||||
|
const fragment = JSDOM.fragment(html);
|
||||||
|
return {
|
||||||
|
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content,
|
||||||
|
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
||||||
|
return fetch(new URL('/oauth/decision', host), {
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
transaction_id: transactionId,
|
||||||
|
login_token: user.token,
|
||||||
|
cancel: cancel ? 'cancel' : '',
|
||||||
|
}),
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
|
||||||
|
const { transactionId } = getMeta(await response.text());
|
||||||
|
assert.ok(transactionId);
|
||||||
|
|
||||||
|
return await fetchDecision(transactionId, user, { cancel });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope,
|
||||||
|
state: 'state',
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
|
const decisionResponse = await fetchDecisionFromResponse(response, user);
|
||||||
|
assert.strictEqual(decisionResponse.status, 302);
|
||||||
|
|
||||||
|
const locationHeader = decisionResponse.headers.get('location');
|
||||||
|
assert.ok(locationHeader);
|
||||||
|
|
||||||
|
const location = new URL(locationHeader);
|
||||||
|
assert.ok(location.searchParams.has('code'));
|
||||||
|
|
||||||
|
const code = new URL(location).searchParams.get('code');
|
||||||
|
assert.ok(code);
|
||||||
|
|
||||||
|
return { client, code };
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIndirectError(response: Response, error: string): void {
|
||||||
|
assert.strictEqual(response.status, 302);
|
||||||
|
|
||||||
|
const locationHeader = response.headers.get('location');
|
||||||
|
assert.ok(locationHeader);
|
||||||
|
|
||||||
|
const location = new URL(locationHeader);
|
||||||
|
assert.strictEqual(location.searchParams.get('error'), error);
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
|
||||||
|
assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local');
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2.1
|
||||||
|
assert.ok(location.searchParams.has('state'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertDirectError(response: Response, status: number, error: string): Promise<void> {
|
||||||
|
assert.strictEqual(response.status, status);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
assert.strictEqual(data.error, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OAuth', () => {
|
||||||
|
let app: INestApplicationContext;
|
||||||
|
let fastify: FastifyInstance;
|
||||||
|
|
||||||
|
let alice: misskey.entities.MeSignup;
|
||||||
|
let bob: misskey.entities.MeSignup;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await startServer();
|
||||||
|
alice = await signup({ username: 'alice' });
|
||||||
|
bob = await signup({ username: 'bob' });
|
||||||
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '';
|
||||||
|
fastify = Fastify();
|
||||||
|
fastify.get('/', async (request, reply) => {
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="redirect_uri" href="/redirect" />
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
await fastify.listen({ port: clientPort });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fastify.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Full flow', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
|
const meta = getMeta(await response.text());
|
||||||
|
assert.strictEqual(typeof meta.transactionId, 'string');
|
||||||
|
assert.ok(meta.transactionId);
|
||||||
|
assert.strictEqual(meta.clientName, 'Misklient');
|
||||||
|
|
||||||
|
const decisionResponse = await fetchDecision(meta.transactionId, alice);
|
||||||
|
assert.strictEqual(decisionResponse.status, 302);
|
||||||
|
assert.ok(decisionResponse.headers.has('location'));
|
||||||
|
|
||||||
|
const locationHeader = decisionResponse.headers.get('location');
|
||||||
|
assert.ok(locationHeader);
|
||||||
|
|
||||||
|
const location = new URL(locationHeader);
|
||||||
|
assert.strictEqual(location.origin + location.pathname, redirect_uri);
|
||||||
|
assert.ok(location.searchParams.has('code'));
|
||||||
|
assert.strictEqual(location.searchParams.get('state'), 'state');
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
|
||||||
|
assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local');
|
||||||
|
|
||||||
|
const code = new URL(location).searchParams.get('code');
|
||||||
|
assert.ok(code);
|
||||||
|
|
||||||
|
const token = await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
assert.strictEqual(typeof token.token.access_token, 'string');
|
||||||
|
assert.strictEqual(token.token.token_type, 'Bearer');
|
||||||
|
assert.strictEqual(token.token.scope, 'write:notes');
|
||||||
|
|
||||||
|
const createResult = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: token.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResult.status, 200);
|
||||||
|
|
||||||
|
const createResultBody = createResult.body as misskey.Endpoints['notes/create']['res'];
|
||||||
|
assert.strictEqual(createResultBody.createdNote.text, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Two concurrent flows', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const pkceAlice = await pkceChallenge(128);
|
||||||
|
const pkceBob = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const responseAlice = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: pkceAlice.code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(responseAlice.status, 200);
|
||||||
|
|
||||||
|
const responseBob = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: pkceBob.code_challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(responseBob.status, 200);
|
||||||
|
|
||||||
|
const decisionResponseAlice = await fetchDecisionFromResponse(responseAlice, alice);
|
||||||
|
assert.strictEqual(decisionResponseAlice.status, 302);
|
||||||
|
|
||||||
|
const decisionResponseBob = await fetchDecisionFromResponse(responseBob, bob);
|
||||||
|
assert.strictEqual(decisionResponseBob.status, 302);
|
||||||
|
|
||||||
|
const locationHeaderAlice = decisionResponseAlice.headers.get('location');
|
||||||
|
assert.ok(locationHeaderAlice);
|
||||||
|
const locationAlice = new URL(locationHeaderAlice);
|
||||||
|
|
||||||
|
const locationHeaderBob = decisionResponseBob.headers.get('location');
|
||||||
|
assert.ok(locationHeaderBob);
|
||||||
|
const locationBob = new URL(locationHeaderBob);
|
||||||
|
|
||||||
|
const codeAlice = locationAlice.searchParams.get('code');
|
||||||
|
assert.ok(codeAlice);
|
||||||
|
const codeBob = locationBob.searchParams.get('code');
|
||||||
|
assert.ok(codeBob);
|
||||||
|
|
||||||
|
const tokenAlice = await client.getToken({
|
||||||
|
code: codeAlice,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier: pkceAlice.code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
|
||||||
|
const tokenBob = await client.getToken({
|
||||||
|
code: codeBob,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier: pkceBob.code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
|
||||||
|
const createResultAlice = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: tokenAlice.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResultAlice.status, 200);
|
||||||
|
|
||||||
|
const createResultBob = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: tokenBob.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResultAlice.status, 200);
|
||||||
|
|
||||||
|
const createResultBodyAlice = await createResultAlice.body as misskey.Endpoints['notes/create']['res'];
|
||||||
|
assert.strictEqual(createResultBodyAlice.createdNote.user.username, 'alice');
|
||||||
|
|
||||||
|
const createResultBodyBob = await createResultBob.body as misskey.Endpoints['notes/create']['res'];
|
||||||
|
assert.strictEqual(createResultBodyBob.createdNote.user.username, 'bob');
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7636.html
|
||||||
|
describe('PKCE', () => {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.4.1
|
||||||
|
// '... the authorization endpoint MUST return the authorization
|
||||||
|
// error response with the "error" value set to "invalid_request".'
|
||||||
|
test('Require PKCE', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
// Pattern 1: No PKCE fields at all
|
||||||
|
let response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
}), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_request');
|
||||||
|
|
||||||
|
// Pattern 2: Only code_challenge
|
||||||
|
response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_request');
|
||||||
|
|
||||||
|
// Pattern 3: Only code_challenge_method
|
||||||
|
response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_request');
|
||||||
|
|
||||||
|
// Pattern 4: Unsupported code_challenge_method
|
||||||
|
response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'SSSS',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use precomputed challenge/verifier set here for deterministic test
|
||||||
|
const code_challenge = '4w2GDuvaxXlw2l46k5PFIoIcTGHdzw2i3hrn-C_Q6f7u0-nTYKd-beVEYy9XinYsGtAix.Nnvr.GByD3lAii2ibPRsSDrZgIN0YQb.kfevcfR9aDKoTLyOUm4hW4ABhs';
|
||||||
|
const code_verifier = 'Ew8VSBiH59JirLlg7ocFpLQ6NXuFC1W_rn8gmRzBKc8';
|
||||||
|
|
||||||
|
const tests: Record<string, string | undefined> = {
|
||||||
|
'Code followed by some junk code': code_verifier + 'x',
|
||||||
|
'Clipped code': code_verifier.slice(0, 80),
|
||||||
|
'Some part of code is replaced': code_verifier.slice(0, -10) + 'x'.repeat(10),
|
||||||
|
'No verifier': undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Verify PKCE', () => {
|
||||||
|
for (const [title, wrong_verifier] of Object.entries(tests)) {
|
||||||
|
test(title, async () => {
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier: wrong_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2
|
||||||
|
// "If an authorization code is used more than once, the authorization server
|
||||||
|
// MUST deny the request and SHOULD revoke (when possible) all tokens
|
||||||
|
// previously issued based on that authorization code."
|
||||||
|
describe('Revoking authorization code', () => {
|
||||||
|
test('On success', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('On failure', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({ code, redirect_uri }), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Revoke the already granted access token', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
const token = await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
|
||||||
|
const createResult = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: token.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResult.status, 200);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const createResult2 = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: token.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResult2.status, 401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cancellation', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
|
const decisionResponse = await fetchDecisionFromResponse(response, alice, { cancel: true });
|
||||||
|
assert.strictEqual(decisionResponse.status, 302);
|
||||||
|
|
||||||
|
const locationHeader = decisionResponse.headers.get('location');
|
||||||
|
assert.ok(locationHeader);
|
||||||
|
|
||||||
|
const location = new URL(locationHeader);
|
||||||
|
assert.ok(!location.searchParams.has('code'));
|
||||||
|
assert.ok(location.searchParams.has('error'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.3
|
||||||
|
describe('Scope', () => {
|
||||||
|
// "If the client omits the scope parameter when requesting
|
||||||
|
// authorization, the authorization server MUST either process the
|
||||||
|
// request using a pre-defined default value or fail the request
|
||||||
|
// indicating an invalid scope."
|
||||||
|
// (And Misskey does the latter)
|
||||||
|
test('Missing scope', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Empty scope', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: '',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unknown scopes', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'test:unknown test:unknown2',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended), { redirect: 'manual' });
|
||||||
|
assertIndirectError(response, 'invalid_scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
// "If the issued access token scope
|
||||||
|
// is different from the one requested by the client, the authorization
|
||||||
|
// server MUST include the "scope" response parameter to inform the
|
||||||
|
// client of the actual scope granted."
|
||||||
|
// (Although Misskey always return scope, which is also fine)
|
||||||
|
test('Partially known scopes', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
// Just get the known scope for this case for backward compatibility
|
||||||
|
const { client, code } = await fetchAuthorizationCode(
|
||||||
|
alice,
|
||||||
|
'write:notes test:unknown test:unknown2',
|
||||||
|
code_challenge,
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
|
||||||
|
assert.strictEqual(token.token.scope, 'write:notes');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Known scopes', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes read:account',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Duplicated scopes', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const { client, code } = await fetchAuthorizationCode(
|
||||||
|
alice,
|
||||||
|
'write:notes write:notes read:account read:account',
|
||||||
|
code_challenge,
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
assert.strictEqual(token.token.scope, 'write:notes read:account');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Scope check by API', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'read:account', code_challenge);
|
||||||
|
|
||||||
|
const token = await client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended);
|
||||||
|
assert.strictEqual(typeof token.token.access_token, 'string');
|
||||||
|
|
||||||
|
const createResult = await api('notes/create', { text: 'test' }, {
|
||||||
|
token: token.token.access_token as string,
|
||||||
|
bearer: true,
|
||||||
|
});
|
||||||
|
assert.strictEqual(createResult.status, 403);
|
||||||
|
assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="insufficient_scope", error_description'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.4
|
||||||
|
// "If an authorization request fails validation due to a missing,
|
||||||
|
// invalid, or mismatching redirection URI, the authorization server
|
||||||
|
// SHOULD inform the resource owner of the error and MUST NOT
|
||||||
|
// automatically redirect the user-agent to the invalid redirection URI."
|
||||||
|
describe('Redirection', () => {
|
||||||
|
test('Invalid redirect_uri at authorization endpoint', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri: 'http://127.0.0.2/',
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid redirect_uri including the valid one at authorization endpoint', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri: 'http://127.0.0.1/redirection',
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('No redirect_uri at authorization endpoint', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid redirect_uri at token endpoint', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri: 'http://127.0.0.2/',
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid redirect_uri including the valid one at token endpoint', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
redirect_uri: 'http://127.0.0.1/redirection',
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('No redirect_uri at token endpoint', async () => {
|
||||||
|
const { code_challenge, code_verifier } = await pkceChallenge(128);
|
||||||
|
|
||||||
|
const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge);
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
code,
|
||||||
|
code_verifier,
|
||||||
|
} as AuthorizationTokenConfigExtended), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'invalid_grant');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8414
|
||||||
|
test('Server metadata', async () => {
|
||||||
|
const response = await fetch(new URL('.well-known/oauth-authorization-server', host));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
assert.strictEqual(body.issuer, 'http://misskey.local');
|
||||||
|
assert.ok(body.scopes_supported.includes('write:notes'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any error on decision endpoint is solely on Misskey side and nothing to do with the client.
|
||||||
|
// Do not use indirect error here.
|
||||||
|
describe('Decision endpoint', () => {
|
||||||
|
test('No login token', async () => {
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL(basicAuthParams));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
|
||||||
|
const { transactionId } = getMeta(await response.text());
|
||||||
|
assert.ok(transactionId);
|
||||||
|
|
||||||
|
const decisionResponse = await fetch(new URL('/oauth/decision', host), {
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
transaction_id: transactionId,
|
||||||
|
}),
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await assertDirectError(decisionResponse, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('No transaction ID', async () => {
|
||||||
|
const decisionResponse = await fetch(new URL('/oauth/decision', host), {
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
login_token: alice.token,
|
||||||
|
}),
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await assertDirectError(decisionResponse, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Invalid transaction ID', async () => {
|
||||||
|
const decisionResponse = await fetch(new URL('/oauth/decision', host), {
|
||||||
|
method: 'post',
|
||||||
|
body: new URLSearchParams({
|
||||||
|
login_token: alice.token,
|
||||||
|
transaction_id: 'invalid_id',
|
||||||
|
}),
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await assertDirectError(decisionResponse, 403, 'access_denied');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only authorization code grant is supported
|
||||||
|
describe('Grant type', () => {
|
||||||
|
test('Implicit grant is not supported', async () => {
|
||||||
|
const url = new URL('/oauth/authorize', host);
|
||||||
|
url.searchParams.append('response_type', 'token');
|
||||||
|
const response = await fetch(url);
|
||||||
|
assertDirectError(response, 501, 'unsupported_response_type');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Resource owner grant is not supported', async () => {
|
||||||
|
const client = new ResourceOwnerPassword({
|
||||||
|
...clientConfig,
|
||||||
|
auth: {
|
||||||
|
tokenHost: host,
|
||||||
|
tokenPath: '/oauth/token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({
|
||||||
|
username: 'alice',
|
||||||
|
password: 'test',
|
||||||
|
}), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'unsupported_grant_type');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Client credential grant is not supported', async () => {
|
||||||
|
const client = new ClientCredentials({
|
||||||
|
...clientConfig,
|
||||||
|
auth: {
|
||||||
|
tokenHost: host,
|
||||||
|
tokenPath: '/oauth/token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(client.getToken({}), (err: GetTokenError) => {
|
||||||
|
assert.strictEqual(err.data.payload.error, 'unsupported_grant_type');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://indieauth.spec.indieweb.org/#client-information-discovery
|
||||||
|
describe('Client Information Discovery', () => {
|
||||||
|
describe('Redirection', () => {
|
||||||
|
const tests: Record<string, (reply: FastifyReply) => void> = {
|
||||||
|
'Read HTTP header': reply => {
|
||||||
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Mixed links': reply => {
|
||||||
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="redirect_uri" href="/redirect2" />
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Multiple items in Link header': reply => {
|
||||||
|
reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
'Multiple items in HTML': reply => {
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<link rel="redirect_uri" href="/redirect2" />
|
||||||
|
<link rel="redirect_uri" href="/redirect" />
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [title, replyFunc] of Object.entries(tests)) {
|
||||||
|
test(title, async () => {
|
||||||
|
await fastify.close();
|
||||||
|
|
||||||
|
fastify = Fastify();
|
||||||
|
fastify.get('/', async (request, reply) => replyFunc(reply));
|
||||||
|
await fastify.listen({ port: clientPort });
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('No item', async () => {
|
||||||
|
await fastify.close();
|
||||||
|
|
||||||
|
fastify = Fastify();
|
||||||
|
fastify.get('/', async (request, reply) => {
|
||||||
|
reply.send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<div class="h-app"><div class="p-name">Misklient
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
await fastify.listen({ port: clientPort });
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
|
||||||
|
// direct error because there's no redirect URI to ping
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Disallow loopback', async () => {
|
||||||
|
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1';
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
await assertDirectError(response, 400, 'invalid_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Missing name', async () => {
|
||||||
|
await fastify.close();
|
||||||
|
|
||||||
|
fastify = Fastify();
|
||||||
|
fastify.get('/', async (request, reply) => {
|
||||||
|
reply.header('Link', '</redirect>; rel="redirect_uri"');
|
||||||
|
reply.send();
|
||||||
|
});
|
||||||
|
await fastify.listen({ port: clientPort });
|
||||||
|
|
||||||
|
const client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
|
const response = await fetch(client.authorizeURL({
|
||||||
|
redirect_uri,
|
||||||
|
scope: 'write:notes',
|
||||||
|
state: 'state',
|
||||||
|
code_challenge: 'code',
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
} as AuthorizationParamsExtended));
|
||||||
|
assert.strictEqual(response.status, 200);
|
||||||
|
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unknown OAuth endpoint', async () => {
|
||||||
|
const response = await fetch(new URL('/oauth/foo', host));
|
||||||
|
assert.strictEqual(response.status, 404);
|
||||||
|
});
|
||||||
|
});
|
|
@ -90,7 +90,7 @@ const request = async (path: string, params: any, me?: UserToken): Promise<{ sta
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const relativeFetch = async (path: string, init?: RequestInit | undefined) => {
|
export const relativeFetch = async (path: string, init?: RequestInit | undefined) => {
|
||||||
return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init);
|
return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
ref="el" class="_button"
|
ref="el" class="_button"
|
||||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||||
:type="type"
|
:type="type"
|
||||||
|
:name="name"
|
||||||
|
:value="value"
|
||||||
@click="emit('click', $event)"
|
@click="emit('click', $event)"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
>
|
>
|
||||||
|
@ -44,6 +46,8 @@ const props = defineProps<{
|
||||||
large?: boolean;
|
large?: boolean;
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
asLike?: boolean;
|
asLike?: boolean;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><MkPageHeader/></template>
|
||||||
|
<MkSpacer :contentMax="800">
|
||||||
|
<div v-if="$i">
|
||||||
|
<div v-if="permissions.length > 0">
|
||||||
|
<p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
|
||||||
|
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="p in permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
|
||||||
|
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
|
||||||
|
<form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
|
||||||
|
<input name="login_token" type="hidden" :value="$i.token"/>
|
||||||
|
<input name="transaction_id" type="hidden" :value="transactionIdMeta?.content"/>
|
||||||
|
<MkButton inline name="cancel" value="cancel">{{ i18n.ts.cancel }}</MkButton>
|
||||||
|
<MkButton inline primary>{{ i18n.ts.accept }}</MkButton>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p :class="$style.loginMessage">{{ i18n.ts._auth.pleaseLogin }}</p>
|
||||||
|
<MkSignin @login="onLogin"/>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import MkSignin from '@/components/MkSignin.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import { $i, login } from '@/account';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
|
||||||
|
if (transactionIdMeta) {
|
||||||
|
transactionIdMeta.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content;
|
||||||
|
const permissions = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:scope"]')?.content.split(' ') ?? [];
|
||||||
|
|
||||||
|
function onLogin(res): void {
|
||||||
|
login(res.i);
|
||||||
|
}
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: 'OAuth',
|
||||||
|
icon: 'ti ti-apps',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.buttons {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginMessage {
|
||||||
|
text-align: center;
|
||||||
|
margin: 8px 0 24px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -254,6 +254,9 @@ export const routes = [{
|
||||||
icon: 'icon',
|
icon: 'icon',
|
||||||
permission: 'permission',
|
permission: 'permission',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
path: '/oauth/authorize',
|
||||||
|
component: page(() => import('./pages/oauth.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/tags/:tag',
|
path: '/tags/:tag',
|
||||||
component: page(() => import('./pages/tag.vue')),
|
component: page(() => import('./pages/tag.vue')),
|
||||||
|
|
184
pnpm-lock.yaml
184
pnpm-lock.yaml
|
@ -98,6 +98,9 @@ importers:
|
||||||
'@fastify/cors':
|
'@fastify/cors':
|
||||||
specifier: 8.3.0
|
specifier: 8.3.0
|
||||||
version: 8.3.0
|
version: 8.3.0
|
||||||
|
'@fastify/express':
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0
|
||||||
'@fastify/http-proxy':
|
'@fastify/http-proxy':
|
||||||
specifier: 9.2.1
|
specifier: 9.2.1
|
||||||
version: 9.2.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
|
version: 9.2.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
|
||||||
|
@ -149,6 +152,9 @@ importers:
|
||||||
blurhash:
|
blurhash:
|
||||||
specifier: 2.0.5
|
specifier: 2.0.5
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
|
body-parser:
|
||||||
|
specifier: ^1.20.2
|
||||||
|
version: 1.20.2
|
||||||
bullmq:
|
bullmq:
|
||||||
specifier: 4.1.0
|
specifier: 4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
@ -209,6 +215,9 @@ importers:
|
||||||
hpagent:
|
hpagent:
|
||||||
specifier: 1.2.0
|
specifier: 1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
http-link-header:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
ioredis:
|
ioredis:
|
||||||
specifier: 5.3.2
|
specifier: 5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
|
@ -266,6 +275,12 @@ importers:
|
||||||
oauth:
|
oauth:
|
||||||
specifier: 0.10.0
|
specifier: 0.10.0
|
||||||
version: 0.10.0
|
version: 0.10.0
|
||||||
|
oauth2orize:
|
||||||
|
specifier: ^1.11.1
|
||||||
|
version: 1.11.1
|
||||||
|
oauth2orize-pkce:
|
||||||
|
specifier: ^0.1.2
|
||||||
|
version: 0.1.2
|
||||||
os-utils:
|
os-utils:
|
||||||
specifier: 0.0.14
|
specifier: 0.0.14
|
||||||
version: 0.0.14
|
version: 0.0.14
|
||||||
|
@ -278,6 +293,9 @@ importers:
|
||||||
pg:
|
pg:
|
||||||
specifier: 8.11.0
|
specifier: 8.11.0
|
||||||
version: 8.11.0
|
version: 8.11.0
|
||||||
|
pkce-challenge:
|
||||||
|
specifier: ^4.0.1
|
||||||
|
version: 4.0.1
|
||||||
probe-image-size:
|
probe-image-size:
|
||||||
specifier: 7.2.3
|
specifier: 7.2.3
|
||||||
version: 7.2.3
|
version: 7.2.3
|
||||||
|
@ -490,6 +508,9 @@ importers:
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
specifier: 2.4.2
|
specifier: 2.4.2
|
||||||
version: 2.4.2
|
version: 2.4.2
|
||||||
|
'@types/body-parser':
|
||||||
|
specifier: ^1.19.2
|
||||||
|
version: 1.19.2
|
||||||
'@types/cbor':
|
'@types/cbor':
|
||||||
specifier: 6.0.0
|
specifier: 6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
@ -505,6 +526,9 @@ importers:
|
||||||
'@types/fluent-ffmpeg':
|
'@types/fluent-ffmpeg':
|
||||||
specifier: 2.1.21
|
specifier: 2.1.21
|
||||||
version: 2.1.21
|
version: 2.1.21
|
||||||
|
'@types/http-link-header':
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: 29.5.2
|
specifier: 29.5.2
|
||||||
version: 29.5.2
|
version: 29.5.2
|
||||||
|
@ -538,6 +562,9 @@ importers:
|
||||||
'@types/oauth':
|
'@types/oauth':
|
||||||
specifier: 0.9.1
|
specifier: 0.9.1
|
||||||
version: 0.9.1
|
version: 0.9.1
|
||||||
|
'@types/oauth2orize':
|
||||||
|
specifier: ^1.11.0
|
||||||
|
version: 1.11.0
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: 8.10.2
|
specifier: 8.10.2
|
||||||
version: 8.10.2
|
version: 8.10.2
|
||||||
|
@ -571,6 +598,9 @@ importers:
|
||||||
'@types/sharp':
|
'@types/sharp':
|
||||||
specifier: 0.32.0
|
specifier: 0.32.0
|
||||||
version: 0.32.0
|
version: 0.32.0
|
||||||
|
'@types/simple-oauth2':
|
||||||
|
specifier: ^5.0.4
|
||||||
|
version: 5.0.4
|
||||||
'@types/sinonjs__fake-timers':
|
'@types/sinonjs__fake-timers':
|
||||||
specifier: 8.1.2
|
specifier: 8.1.2
|
||||||
version: 8.1.2
|
version: 8.1.2
|
||||||
|
@ -625,6 +655,9 @@ importers:
|
||||||
jest-mock:
|
jest-mock:
|
||||||
specifier: 29.5.0
|
specifier: 29.5.0
|
||||||
version: 29.5.0
|
version: 29.5.0
|
||||||
|
simple-oauth2:
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: 5.0.0
|
||||||
|
|
||||||
packages/frontend:
|
packages/frontend:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4802,6 +4835,15 @@ packages:
|
||||||
resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==}
|
resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@fastify/express@2.3.0:
|
||||||
|
resolution: {integrity: sha512-jvvjlPPCfJsSHfF6tQDyARJ3+c3xXiqcxVZu6bi3xMWCWB3fl07vrjFDeaqnwqKhLZ9+m6cog5dw7gIMKEsTnQ==}
|
||||||
|
dependencies:
|
||||||
|
express: 4.18.2
|
||||||
|
fastify-plugin: 4.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@fastify/fast-json-stringify-compiler@4.3.0:
|
/@fastify/fast-json-stringify-compiler@4.3.0:
|
||||||
resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
|
resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4898,6 +4940,24 @@ packages:
|
||||||
hashlru: 2.3.0
|
hashlru: 2.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/boom@10.0.1:
|
||||||
|
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 11.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@hapi/bourne@3.0.0:
|
||||||
|
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@hapi/hoek@10.0.1:
|
||||||
|
resolution: {integrity: sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@hapi/hoek@11.0.2:
|
||||||
|
resolution: {integrity: sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@hapi/hoek@9.3.0:
|
/@hapi/hoek@9.3.0:
|
||||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4908,6 +4968,14 @@ packages:
|
||||||
'@hapi/hoek': 9.3.0
|
'@hapi/hoek': 9.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@hapi/wreck@18.0.1:
|
||||||
|
resolution: {integrity: sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 10.0.1
|
||||||
|
'@hapi/bourne': 3.0.0
|
||||||
|
'@hapi/hoek': 11.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.10:
|
/@humanwhocodes/config-array@0.11.10:
|
||||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
|
@ -7619,6 +7687,12 @@ packages:
|
||||||
/@types/http-cache-semantics@4.0.1:
|
/@types/http-cache-semantics@4.0.1:
|
||||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||||
|
|
||||||
|
/@types/http-link-header@1.0.3:
|
||||||
|
resolution: {integrity: sha512-y8HkoD/vyid+5MrJ3aas0FvU3/BVBGcyG9kgxL0Zn4JwstA8CglFPnrR0RuzOjRCXwqzL5uxWC2IO7Ub0rMU2A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.3.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/istanbul-lib-coverage@2.0.4:
|
/@types/istanbul-lib-coverage@2.0.4:
|
||||||
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
|
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -7762,6 +7836,13 @@ packages:
|
||||||
resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==}
|
resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/oauth2orize@1.11.0:
|
||||||
|
resolution: {integrity: sha512-jmnP/Ip36XBzs+nIn/I8wNBZkQcn/agmp8K9V81he+wOllLYMec8T8AqbRPJCFbnFwaL03bbR8gI3CknMCXohw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.17
|
||||||
|
'@types/node': 20.3.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/oauth@0.9.1:
|
/@types/oauth@0.9.1:
|
||||||
resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==}
|
resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7898,6 +7979,10 @@ packages:
|
||||||
sharp: 0.32.1
|
sharp: 0.32.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/simple-oauth2@5.0.4:
|
||||||
|
resolution: {integrity: sha512-4SvTfmAa1fGUa1d07j9vIiC4o92bGh0ihPXmtS05udMMmNwVIaU2nZ706cC4wI8cJxOlHD4P/d5tzqvWYd+KxA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/sinon@10.0.13:
|
/@types/sinon@10.0.13:
|
||||||
resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==}
|
resolution: {integrity: sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8820,7 +8905,6 @@ packages:
|
||||||
|
|
||||||
/array-flatten@1.1.1:
|
/array-flatten@1.1.1:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/array-includes@3.1.6:
|
/array-includes@3.1.6:
|
||||||
resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==}
|
resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==}
|
||||||
|
@ -9416,7 +9500,26 @@ packages:
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
/body-parser@1.20.2:
|
||||||
|
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
|
||||||
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 2.6.9
|
||||||
|
depd: 2.0.0
|
||||||
|
destroy: 1.2.0
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
on-finished: 2.4.1
|
||||||
|
qs: 6.11.0
|
||||||
|
raw-body: 2.5.2
|
||||||
|
type-is: 1.6.18
|
||||||
|
unpipe: 1.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/boolbase@1.0.0:
|
/boolbase@1.0.0:
|
||||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||||
|
@ -9617,7 +9720,6 @@ packages:
|
||||||
/bytes@3.1.2:
|
/bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/c8@7.13.0:
|
/c8@7.13.0:
|
||||||
resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==}
|
resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==}
|
||||||
|
@ -10398,7 +10500,6 @@ packages:
|
||||||
/content-type@1.0.5:
|
/content-type@1.0.5:
|
||||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/convert-source-map@1.9.0:
|
/convert-source-map@1.9.0:
|
||||||
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
|
||||||
|
@ -10409,7 +10510,6 @@ packages:
|
||||||
|
|
||||||
/cookie-signature@1.0.6:
|
/cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/cookie@0.4.2:
|
/cookie@0.4.2:
|
||||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||||
|
@ -10952,7 +11052,6 @@ packages:
|
||||||
/destroy@1.2.0:
|
/destroy@1.2.0:
|
||||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/detect-file@1.0.0:
|
/detect-file@1.0.0:
|
||||||
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
|
resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
|
||||||
|
@ -11132,7 +11231,6 @@ packages:
|
||||||
|
|
||||||
/ee-first@1.1.1:
|
/ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ejs@3.1.8:
|
/ejs@3.1.8:
|
||||||
resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
|
resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==}
|
||||||
|
@ -11167,7 +11265,6 @@ packages:
|
||||||
/encodeurl@1.0.2:
|
/encodeurl@1.0.2:
|
||||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/encoding@0.1.13:
|
/encoding@0.1.13:
|
||||||
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
||||||
|
@ -11665,7 +11762,6 @@ packages:
|
||||||
/etag@1.8.1:
|
/etag@1.8.1:
|
||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/event-stream@3.3.4:
|
/event-stream@3.3.4:
|
||||||
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
|
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
|
||||||
|
@ -11845,7 +11941,6 @@ packages:
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ext-list@2.2.2:
|
/ext-list@2.2.2:
|
||||||
resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
|
resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
|
||||||
|
@ -12181,7 +12276,6 @@ packages:
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/find-cache-dir@2.1.0:
|
/find-cache-dir@2.1.0:
|
||||||
resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
|
resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==}
|
||||||
|
@ -12422,7 +12516,6 @@ packages:
|
||||||
/fresh@0.5.2:
|
/fresh@0.5.2:
|
||||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/from@0.1.7:
|
/from@0.1.7:
|
||||||
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
|
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
|
||||||
|
@ -13248,6 +13341,11 @@ packages:
|
||||||
statuses: 2.0.1
|
statuses: 2.0.1
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
/http-link-header@1.1.0:
|
||||||
|
resolution: {integrity: sha512-pj6N1yxOz/ANO8HHsWGg/OoIL1kmRYvQnXQ7PIRpgp+15AnEsRH8fmIJE6D1OdWG2Bov+BJHVla1fFXxg1JbbA==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-proxy-agent@5.0.0:
|
/http-proxy-agent@5.0.0:
|
||||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -15377,7 +15475,6 @@ packages:
|
||||||
/media-typer@0.3.0:
|
/media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/meilisearch@0.33.0:
|
/meilisearch@0.33.0:
|
||||||
resolution: {integrity: sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==}
|
resolution: {integrity: sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w==}
|
||||||
|
@ -15413,7 +15510,6 @@ packages:
|
||||||
|
|
||||||
/merge-descriptors@1.0.1:
|
/merge-descriptors@1.0.1:
|
||||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/merge-stream@2.0.0:
|
/merge-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
|
@ -15425,7 +15521,6 @@ packages:
|
||||||
/methods@1.1.2:
|
/methods@1.1.2:
|
||||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mfm-js@0.23.3:
|
/mfm-js@0.23.3:
|
||||||
resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==}
|
resolution: {integrity: sha512-o8scYmbey6rMUmWAlT3k3ntt6khaCLdxlmHhAWV5wTTMj2OK1atQvZfRUq0SIVm1Jig08qlZg/ps71xUqrScNA==}
|
||||||
|
@ -15474,7 +15569,6 @@ packages:
|
||||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime@2.6.0:
|
/mime@2.6.0:
|
||||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||||
|
@ -16156,6 +16250,21 @@ packages:
|
||||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/oauth2orize-pkce@0.1.2:
|
||||||
|
resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/oauth2orize@1.11.1:
|
||||||
|
resolution: {integrity: sha512-9dSx/Gwm0J2Rvj4RH9+h7iXVnRXZ6biwWRgb2dCeQhCosODS0nYdM9I/G7BUGsjbgn0pHjGcn1zcCRtzj2SlRA==}
|
||||||
|
engines: {node: '>= 0.4.0'}
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
uid2: 0.0.4
|
||||||
|
utils-merge: 1.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/oauth@0.10.0:
|
/oauth@0.10.0:
|
||||||
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
|
resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -16178,7 +16287,6 @@ packages:
|
||||||
|
|
||||||
/object-inspect@1.12.2:
|
/object-inspect@1.12.2:
|
||||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/object-is@1.1.5:
|
/object-is@1.1.5:
|
||||||
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
|
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
|
||||||
|
@ -16273,7 +16381,6 @@ packages:
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/on-headers@1.0.2:
|
/on-headers@1.0.2:
|
||||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||||
|
@ -16583,7 +16690,6 @@ packages:
|
||||||
/parseurl@1.3.3:
|
/parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pascalcase@0.1.1:
|
/pascalcase@0.1.1:
|
||||||
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
|
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
|
||||||
|
@ -16652,7 +16758,6 @@ packages:
|
||||||
|
|
||||||
/path-to-regexp@0.1.7:
|
/path-to-regexp@0.1.7:
|
||||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-to-regexp@1.8.0:
|
/path-to-regexp@1.8.0:
|
||||||
resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
|
resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
|
||||||
|
@ -16860,6 +16965,11 @@ packages:
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pkce-challenge@4.0.1:
|
||||||
|
resolution: {integrity: sha512-WGmtS1stcStsvRwNXix3iR1ujFcDaJR+sEODRa2ZFruT0lM4lhPAFTL5SUpqD5vTJdRlgtuMQhcp1kIEJx4LUw==}
|
||||||
|
engines: {node: '>=16.20.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pkg-dir@3.0.0:
|
/pkg-dir@3.0.0:
|
||||||
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
|
resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -17566,7 +17676,6 @@ packages:
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.0.4
|
side-channel: 1.0.4
|
||||||
dev: true
|
|
||||||
|
|
||||||
/qs@6.11.1:
|
/qs@6.11.1:
|
||||||
resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==}
|
resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==}
|
||||||
|
@ -17631,7 +17740,6 @@ packages:
|
||||||
/range-parser@1.2.1:
|
/range-parser@1.2.1:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ratelimiter@3.4.1:
|
/ratelimiter@3.4.1:
|
||||||
resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==}
|
resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==}
|
||||||
|
@ -17645,7 +17753,16 @@ packages:
|
||||||
http-errors: 2.0.0
|
http-errors: 2.0.0
|
||||||
iconv-lite: 0.4.24
|
iconv-lite: 0.4.24
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
dev: true
|
|
||||||
|
/raw-body@2.5.2:
|
||||||
|
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
unpipe: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/rc@1.2.8:
|
/rc@1.2.8:
|
||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
|
@ -18483,7 +18600,6 @@ packages:
|
||||||
statuses: 2.0.1
|
statuses: 2.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/serve-favicon@2.5.0:
|
/serve-favicon@2.5.0:
|
||||||
resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==}
|
resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==}
|
||||||
|
@ -18506,7 +18622,6 @@ packages:
|
||||||
send: 0.18.0
|
send: 0.18.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/set-blocking@2.0.0:
|
/set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
@ -18612,7 +18727,6 @@ packages:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
get-intrinsic: 1.2.0
|
get-intrinsic: 1.2.0
|
||||||
object-inspect: 1.12.2
|
object-inspect: 1.12.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/siginfo@2.0.0:
|
/siginfo@2.0.0:
|
||||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
@ -18640,6 +18754,17 @@ packages:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
simple-concat: 1.0.1
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
|
/simple-oauth2@5.0.0:
|
||||||
|
resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 10.0.1
|
||||||
|
'@hapi/wreck': 18.0.1
|
||||||
|
debug: 4.3.4(supports-color@8.1.1)
|
||||||
|
joi: 17.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/simple-swizzle@0.2.2:
|
/simple-swizzle@0.2.2:
|
||||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -19883,7 +20008,6 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: true
|
|
||||||
|
|
||||||
/type@1.2.0:
|
/type@1.2.0:
|
||||||
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
|
resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==}
|
||||||
|
@ -19998,6 +20122,10 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/uid2@0.0.4:
|
||||||
|
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/uid@2.0.2:
|
/uid@2.0.2:
|
||||||
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -20163,7 +20291,6 @@ packages:
|
||||||
/unpipe@1.0.0:
|
/unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/unplugin@0.10.2:
|
/unplugin@0.10.2:
|
||||||
resolution: {integrity: sha512-6rk7GUa4ICYjae5PrAllvcDeuT8pA9+j5J5EkxbMFaV+SalHhxZ7X2dohMzu6C3XzsMT+6jwR/+pwPNR3uK9MA==}
|
resolution: {integrity: sha512-6rk7GUa4ICYjae5PrAllvcDeuT8pA9+j5J5EkxbMFaV+SalHhxZ7X2dohMzu6C3XzsMT+6jwR/+pwPNR3uK9MA==}
|
||||||
|
@ -20279,7 +20406,6 @@ packages:
|
||||||
/utils-merge@1.0.1:
|
/utils-merge@1.0.1:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/uuid@3.4.0:
|
/uuid@3.4.0:
|
||||||
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
|
resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
|
||||||
|
|
Loading…
Reference in New Issue