1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-27 09:04:40 +02:00
This commit is contained in:
kakkokari-gtyih
2026-05-16 16:01:12 +09:00
parent e6375fb756
commit c2b3ae058d
5 changed files with 639 additions and 350 deletions

View File

@@ -57,7 +57,6 @@
"@aws-sdk/lib-storage": "3.1044.0",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
"@fastify/express": "4.0.5",
"@fastify/http-proxy": "11.4.4",
"@fastify/multipart": "10.0.0",
"@fastify/static": "9.1.3",
@@ -83,7 +82,6 @@
"async-mutex": "0.5.0",
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.2",
"bullmq": "5.76.6",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
@@ -122,12 +120,10 @@
"node-html-parser": "7.1.0",
"nodemailer": "8.0.7",
"nsfwjs": "4.3.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"oidc-provider": "9.8.3",
"os-utils": "0.0.14",
"otpauth": "9.5.1",
"pg": "8.20.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.3.0",
"promise-limit": "2.7.0",
"qrcode": "1.5.4",
@@ -163,7 +159,6 @@
"@sentry/vue": "10.52.0",
"@types/accepts": "1.3.7",
"@types/archiver": "7.0.0",
"@types/body-parser": "1.19.6",
"@types/color-convert": "3.0.1",
"@types/content-disposition": "0.5.9",
"@types/fluent-ffmpeg": "2.1.28",
@@ -174,13 +169,13 @@
"@types/ms": "2.1.0",
"@types/node": "24.12.2",
"@types/nodemailer": "8.0.0",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/oidc-provider": "9.5.0",
"@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"pkce-challenge": "6.0.0",
"@types/sanitize-html": "2.16.1",
"@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.8",

View File

@@ -73,7 +73,6 @@ export default defineConfig((args) => {
'jsdom',
're2',
'ipaddr.js',
'oauth2orize',
'file-type',
];

View File

@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module 'oidc-provider/lib/helpers/redirect_uri.js' {
export default function redirectUri(uri: string, payload: Record<string, string>, mode: 'query' | 'fragment'): string;
}
declare module 'oidc-provider/lib/helpers/pkce.js' {
export default function checkPKCE(verifier: string | undefined, challenge: string | undefined, method: string | undefined): void;
}

View File

@@ -4,18 +4,15 @@
*/
import dns from 'node:dns/promises';
import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as htmlParser from 'node-html-parser';
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 fastifyCors from '@fastify/cors';
import bodyParser from 'body-parser';
import fastifyExpress from '@fastify/express';
import { verifyChallenge } from 'pkce-challenge';
import checkPKCE from 'oidc-provider/lib/helpers/pkce.js';
import { permissions as kinds } from 'misskey-js';
import redirectUri from 'oidc-provider/lib/helpers/redirect_uri.js';
import { errors as oidcErrors } from 'oidc-provider';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js';
@@ -31,8 +28,7 @@ import Logger from '@/logger.js';
import { StatusError } from '@/misc/status-error.js';
import { HtmlTemplateService } from '@/server/web/HtmlTemplateService.js';
import { OAuthPage } from '@/server/web/views/oauth.js';
import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify';
import type { FastifyInstance, FastifyReply } from 'fastify';
// TODO: Consider migrating to @node-oauth/oauth2-server once
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
@@ -46,7 +42,9 @@ function validateClientId(raw: string): URL {
const url = ((): URL => {
try {
return new URL(raw);
} catch { throw new AuthorizationError('client_id must be a valid URL', 'invalid_request'); }
} catch {
throw new oidcErrors.InvalidRequest('client_id must be a valid URL');
}
})();
// "Client identifier URLs MUST have either an https or http scheme"
@@ -56,7 +54,7 @@ function validateClientId(raw: string): URL {
// in Section 1.6 when the requested response type is "code" or "token"'
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');
throw new oidcErrors.InvalidRequest('client_id must be a valid HTTPS URL');
}
// "MUST contain a path component (new URL() implicitly adds one)"
@@ -64,19 +62,19 @@ function validateClientId(raw: string): URL {
// "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');
throw new oidcErrors.InvalidRequest('client_id must not contain dot path segments');
}
// ("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');
throw new oidcErrors.InvalidRequest('client_id must not contain a fragment component');
}
// "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');
throw new oidcErrors.InvalidRequest('client_id must not contain a username or a password');
}
// ("MAY contain a port")
@@ -84,7 +82,7 @@ function validateClientId(raw: string): URL {
// "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');
throw new oidcErrors.InvalidRequest('client_id must have a domain name as a host name');
}
return url;
@@ -97,6 +95,45 @@ interface ClientInformation {
logo: string | null;
}
interface OAuthRequestParameters {
[key: string]: string | string[] | undefined;
}
interface AuthorizationRequest {
clientId: string;
redirectUri: string;
state?: string;
scopes: string[];
codeChallenge: string;
codeChallengeMethod: string;
}
interface AuthorizationRequestSeed {
clientInfo: ClientInformation;
clientId: string;
redirectUri: string;
state?: string;
requestedScope: string[];
codeChallenge?: string;
codeChallengeMethod?: string;
}
interface AuthorizationTransaction {
client: ClientInformation;
request: AuthorizationRequest;
}
interface AuthorizationCodeGrant {
clientId: string;
userId: string;
redirectUri: string;
codeChallenge: string;
scopes: string[];
grantedToken?: string;
revoked?: boolean;
used?: boolean;
}
function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: string): { name: string | null; logo: string | null; } {
let name: string | null = null;
let logo: string | null = null;
@@ -139,7 +176,7 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
// redirect_uris discovered after resolving any relative URLs."
const linkHeader = res.headers.get('link');
if (linkHeader) {
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(link => link.uri));
}
const contentType = res.headers.get('content-type');
@@ -163,13 +200,13 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
// "The authorization server MUST verify that the client_id in the document matches the
// client_id of the URL where the document was retrieved."
if (json.client_id !== id) {
throw new AuthorizationError('client_id in the document does not match the client_id URL', 'invalid_request');
throw new oidcErrors.InvalidRequest('client_id in the document does not match the client_id URL');
}
// https://indieauth.spec.indieweb.org/#client-metadata-li-1
// "The client_uri MUST be a prefix of the client_id."
if (!json.client_uri || !id.startsWith(json.client_uri)) {
throw new AuthorizationError('client_uri is not a prefix of client_id', 'invalid_request');
throw new oidcErrors.InvalidRequest('client_uri is not a prefix of client_id');
}
if (typeof json.client_name === 'string') {
@@ -213,100 +250,126 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
logo,
};
} catch (err) {
console.error(err);
logger.error('Error while fetching client information', { err });
if (err instanceof StatusError) {
throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
} else if (err instanceof AuthorizationError) {
throw err;
} else {
throw new AuthorizationError('Failed to parse client information', 'server_error');
throw new oidcErrors.InvalidRequest('Failed to fetch client information');
}
if (err instanceof oidcErrors.OIDCProviderError) {
throw err;
}
const wrapped = new oidcErrors.InvalidRequest('Failed to parse client information');
wrapped.status = 500;
wrapped.statusCode = 500;
wrapped.error = 'server_error';
throw wrapped;
}
}
type OmitFirstElement<T extends unknown[]> = T extends [unknown, ...(infer R)]
? R
: [];
interface OAuthParsedRequest extends OAuth2Req {
codeChallenge: string;
codeChallengeMethod: string;
function firstValue(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
interface OAuthHttpResponse extends ServerResponse {
redirect(location: string): void;
function normalizeScope(scope: string | string[] | undefined): string[] {
const raw = Array.isArray(scope) ? scope : scope != null ? [scope] : [];
return raw.flatMap(value => value.split(/\s+/)).filter(Boolean);
}
interface OAuth2DecisionRequest extends MiddlewareRequest {
body: {
transaction_id: string;
cancel: boolean;
login_token: string;
function toRequestParameters(body: unknown): OAuthRequestParameters {
if (body == null || typeof body !== 'object' || Array.isArray(body)) {
return {};
}
return body as OAuthRequestParameters;
}
function getQueryMode(issuerUrl: string): oauth2orize.grant.Options['modes'] {
function applyNoStore(reply: FastifyReply): void {
reply.header('Cache-Control', 'no-store');
reply.header('Pragma', 'no-cache');
}
function createUnsupportedResponseTypeError(): oidcErrors.OIDCProviderError {
const error = new oidcErrors.UnsupportedResponseType();
error.status = 501;
error.statusCode = 501;
return error;
}
function createForbiddenAccessDenied(description: string): oidcErrors.OIDCProviderError {
const error = new oidcErrors.AccessDenied(description);
error.status = 403;
error.statusCode = 403;
return error;
}
function normalizeOAuthError(error: unknown): oidcErrors.OIDCProviderError {
if (error instanceof oidcErrors.OIDCProviderError) {
return error;
}
const wrapped = new oidcErrors.InvalidRequest('request is invalid');
if (error instanceof Error) {
wrapped.error_description = error.message;
}
return wrapped;
}
function sendOAuthError(reply: FastifyReply, error: oidcErrors.OIDCProviderError): void {
applyNoStore(reply);
reply.code(error.statusCode ?? error.status ?? 400);
reply.send({
error: error.error,
...(error.expose && error.error_description ? { error_description: error.error_description } : {}),
});
}
function appendIssuer(payload: Record<string, string>, issuerUrl: string): Record<string, string> {
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;
...payload,
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());
},
// 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."
iss: issuerUrl,
};
}
/**
* 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
function redirectWithQuery(reply: FastifyReply, redirectUriString: string, payload: Record<string, string>): void {
applyNoStore(reply);
reply.code(302).redirect(redirectUri(redirectUriString, payload, 'query'));
}
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;
function registerFormBodyParser(fastify: FastifyInstance): void {
if (fastify.hasContentTypeParser('application/x-www-form-urlencoded')) {
return;
}
fastify.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, (_request, body, done) => {
try {
const parsed: OAuthRequestParameters = {};
for (const [key, value] of new URLSearchParams(typeof body === 'string' ? body : body.toString('utf8')).entries()) {
const current = parsed[key];
if (current == null) {
parsed[key] = value;
} else if (Array.isArray(current)) {
current.push(value);
} else {
parsed[key] = [current, value];
}
}
done(null, parsed);
} catch (error) {
done(error as Error, undefined);
}
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);
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(),
});
#authorizationTransactionCache = new MemoryKVCache<AuthorizationTransaction>(1000 * 60 * 5);
#grantCodeCache = new MemoryKVCache<AuthorizationCodeGrant>(1000 * 60 * 5);
#logger: Logger;
constructor(
@@ -314,8 +377,8 @@ export class OAuth2ProviderService {
private config: Config,
private httpRequestService: HttpRequestService,
@Inject(DI.accessTokensRepository)
accessTokensRepository: AccessTokensRepository,
idService: IdService,
private accessTokensRepository: AccessTokensRepository,
private idService: IdService,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private cacheService: CacheService,
@@ -323,108 +386,103 @@ export class OAuth2ProviderService {
private htmlTemplateService: HtmlTemplateService,
) {
this.#logger = loggerService.getLogger('oauth');
}
const grantCodeCache = new MemoryKVCache<{
clientId: string,
userId: string,
redirectUri: string,
codeChallenge: string,
scopes: string[],
async #resolveAuthorizationRequest(params: OAuthRequestParameters): Promise<AuthorizationRequestSeed> {
const clientId = firstValue(params.client_id);
const redirectUriValue = firstValue(params.redirect_uri);
const responseType = firstValue(params.response_type);
const state = firstValue(params.state);
const codeChallenge = firstValue(params.code_challenge);
const codeChallengeMethod = firstValue(params.code_challenge_method);
const requestedScope = normalizeScope(params.scope);
// fields to prevent multiple code use
grantedToken?: string,
revoked?: boolean,
used?: boolean,
}>(1000 * 60 * 5); // expires after 5m
this.#logger.info(`Validating authorization parameters, with client_id: ${clientId}, redirect_uri: ${redirectUriValue}, scope: ${requestedScope.join(' ')}`);
// 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 (responseType !== 'code') {
throw createUnsupportedResponseTypeError();
}
if (!token) {
throw new AuthorizationError('No user', 'invalid_request');
}
const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
() => this.usersRepository.findOneBy({ token }) as Promise<MiLocalUser | null>);
if (!user) {
throw new AuthorizationError('No such user', 'invalid_request');
}
if (!clientId) {
throw new oidcErrors.InvalidRequest('client_id must be provided');
}
this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${client.id} through ${redirectUri}, with scope: [${areq.scope}]`);
const clientUrl = validateClientId(clientId);
const code = secureRndstr(128);
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://indieauth.spec.indieweb.org/#client-information-discovery
// "the server may want to resolve the domain name first and avoid fetching the document
// if the IP address is within the loopback range defined by [RFC5735]
// or any other implementation-specific internal IP address."
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
const lookup = await dns.lookup(clientUrl.hostname);
if (ipaddr.parse(lookup.address).range() !== 'unicast') {
throw new oidcErrors.InvalidRequest('client_id resolves to disallowed IP range.');
}
}
// 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;
// Find client information from the remote.
const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3
if (body.client_id !== granted.clientId) return;
if (redirectUri !== granted.redirectUri) return;
// 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 (!redirectUriValue || !clientInfo.redirectUris.includes(redirectUriValue)) {
throw new oidcErrors.InvalidRequest('Invalid redirect_uri');
}
// 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;
return {
clientInfo,
clientId: clientInfo.id,
redirectUri: redirectUriValue,
state,
requestedScope,
codeChallenge,
codeChallengeMethod,
};
}
const accessToken = secureRndstr(128);
const now = new Date();
#finalizeAuthorizationRequest(seed: AuthorizationRequestSeed): AuthorizationRequest {
const scopes = [...new Set(seed.requestedScope)].filter(scope => (<readonly string[]>kinds).includes(scope));
if (!seed.requestedScope.length || !scopes.length) {
throw new oidcErrors.InvalidScope('`scope` parameter has no known scope', '');
}
// NOTE: we don't have a setup for automatic token expiration
await accessTokensRepository.insert({
id: idService.gen(now.getTime()),
lastUsedAt: now,
userId: granted.userId,
token: accessToken,
hash: accessToken,
name: granted.clientId,
permission: granted.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 seed.codeChallenge !== 'string') {
throw new oidcErrors.InvalidRequest('`code_challenge` parameter is required');
}
if (seed.codeChallengeMethod !== 'S256') {
throw new oidcErrors.InvalidRequest('`code_challenge_method` parameter must be set as S256');
}
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;
}
return {
clientId: seed.clientId,
redirectUri: seed.redirectUri,
state: seed.state,
scopes,
codeChallenge: seed.codeChallenge,
codeChallengeMethod: seed.codeChallengeMethod,
};
}
granted.grantedToken = accessToken;
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
async #findUserByLoginToken(loginToken: string): Promise<MiLocalUser> {
const user = await this.cacheService.localUserByNativeTokenCache.fetch(loginToken,
() => this.usersRepository.findOneBy({ token: loginToken }) as Promise<MiLocalUser | null>);
if (!user) {
throw new oidcErrors.InvalidRequest('No such user');
}
return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
})().then(args => done(null, ...args ?? []), err => done(err));
}));
return user;
}
async #revokeGrantCode(granted: AuthorizationCodeGrant, code: string): Promise<void> {
this.#logger.info(`Detected multiple code use from ${granted.clientId} for user ${granted.userId}. Revoking the code.`);
this.#grantCodeCache.delete(code);
granted.revoked = true;
if (granted.grantedToken) {
await this.accessTokensRepository.delete({ token: granted.grantedToken });
}
}
// https://datatracker.ietf.org/doc/html/rfc8414.html
@@ -445,97 +503,101 @@ export class OAuth2ProviderService {
@bindThis
public async createServer(fastify: FastifyInstance): Promise<void> {
registerFormBodyParser(fastify);
fastify.get('/authorize', async (request, reply) => {
const oauth2 = (request.raw as MiddlewareRequest).oauth2;
if (!oauth2) {
throw new Error('Unexpected lack of authorization information');
let validatedRedirectUri: string | undefined;
let state: string | undefined;
try {
const seed = await this.#resolveAuthorizationRequest(request.query as OAuthRequestParameters);
const authorizationRequest = this.#finalizeAuthorizationRequest(seed);
const { clientInfo } = seed;
validatedRedirectUri = authorizationRequest.redirectUri;
state = authorizationRequest.state;
const transactionId = secureRndstr(128);
this.#authorizationTransactionCache.set(transactionId, {
client: clientInfo,
request: authorizationRequest,
});
this.#logger.info(`Rendering authorization page for "${clientInfo.name}"`);
applyNoStore(reply);
return await HtmlTemplateService.replyHtml(reply, OAuthPage({
...await this.htmlTemplateService.getCommonData(),
transactionId,
clientName: clientInfo.name,
clientLogo: clientInfo.logo ?? undefined,
scope: authorizationRequest.scopes,
}));
} catch (error) {
const oauthError = normalizeOAuthError(error);
if (validatedRedirectUri && oauthError.allow_redirect && oauthError.error !== 'unsupported_response_type') {
redirectWithQuery(reply, validatedRedirectUri, appendIssuer({
error: oauthError.error,
...(state ? { state } : {}),
}, this.config.url));
return;
}
sendOAuthError(reply, oauthError);
}
this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
reply.header('Cache-Control', 'no-store');
return await HtmlTemplateService.replyHtml(reply, OAuthPage({
...await this.htmlTemplateService.getCommonData(),
transactionId: oauth2.transactionID,
clientName: oauth2.client.name,
clientLogo: oauth2.client.logo ?? undefined,
scope: oauth2.req.scope,
}));
});
fastify.post('/decision', async () => { });
await fastify.register(fastifyExpress);
fastify.use('/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);
// https://indieauth.spec.indieweb.org/#client-information-discovery
// "the server may want to resolve the domain name first and avoid fetching the document
// if the IP address is within the loopback range defined by [RFC5735]
// or any other implementation-specific internal IP address."
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
const lookup = await dns.lookup(clientUrl.hostname);
if (ipaddr.parse(lookup.address).range() !== 'unicast') {
throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
}
fastify.post('/decision', async (request, reply) => {
try {
const body = toRequestParameters(request.body);
const transactionId = firstValue(body.transaction_id);
if (!transactionId) {
throw new oidcErrors.InvalidRequest('Missing transaction ID');
}
// Find client information from the remote.
const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
const transaction = this.#authorizationTransactionCache.get(transactionId);
if (!transaction) {
throw createForbiddenAccessDenied('Invalid or expired transaction ID');
}
this.#authorizationTransactionCache.delete(transactionId);
// 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');
const cancel = !!firstValue(body.cancel);
this.#logger.info(`Received the decision. Cancel: ${cancel}`);
if (cancel) {
redirectWithQuery(reply, transaction.request.redirectUri, appendIssuer({
error: 'access_denied',
...(transaction.request.state ? { state: transaction.request.state } : {}),
}, this.config.url));
return;
}
try {
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>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];
const loginToken = firstValue(body.login_token);
if (!loginToken) {
throw new oidcErrors.InvalidRequest('No user');
}
return [null, clientInfo, redirectURI];
})().then(args => done(...args), err => done(err));
}) as ValidateFunctionArity2));
fastify.use('/authorize', this.#server.errorHandler({
mode: 'indirect',
modes: getQueryMode(this.config.url),
}));
fastify.use('/authorize', this.#server.errorHandler());
this.#logger.info(`Checking the user before sending authorization code to ${transaction.client.id}`);
const user = await this.#findUserByLoginToken(loginToken);
fastify.use('/decision', bodyParser.urlencoded({ extended: false }));
fastify.use('/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('/decision', this.#server.errorHandler());
this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${transaction.client.id} through ${transaction.request.redirectUri}, with scope: [${transaction.request.scopes}]`);
const code = secureRndstr(128);
this.#grantCodeCache.set(code, {
clientId: transaction.client.id,
userId: user.id,
redirectUri: transaction.request.redirectUri,
codeChallenge: transaction.request.codeChallenge,
scopes: transaction.request.scopes,
});
redirectWithQuery(reply, transaction.request.redirectUri, appendIssuer({
code,
...(transaction.request.state ? { state: transaction.request.state } : {}),
}, this.config.url));
} catch (error) {
sendOAuthError(reply, normalizeOAuthError(error));
}
});
// Return 404 for any unknown paths under /oauth so that clients can know
// whether a certain endpoint is supported or not.
fastify.all('/*', async (_request, reply) => {
reply.code(404);
reply.send({
@@ -551,14 +613,86 @@ export class OAuth2ProviderService {
@bindThis
public async createTokenServer(fastify: FastifyInstance): Promise<void> {
registerFormBodyParser(fastify);
fastify.register(fastifyCors);
fastify.post('', async () => { });
await fastify.register(fastifyExpress);
// Clients may use JSON or urlencoded
fastify.use('', bodyParser.urlencoded({ extended: false }));
fastify.use('', bodyParser.json({ strict: true }));
fastify.use('', this.#server.token());
fastify.use('', this.#server.errorHandler());
fastify.post('', async (request, reply) => {
applyNoStore(reply);
try {
const body = toRequestParameters(request.body);
const grantType = firstValue(body.grant_type);
if (!grantType) {
throw new oidcErrors.InvalidRequest('grant_type is required');
}
if (grantType !== 'authorization_code') {
throw new oidcErrors.UnsupportedGrantType();
}
const code = firstValue(body.code);
const clientId = firstValue(body.client_id);
const redirectUriValue = firstValue(body.redirect_uri);
const codeVerifier = firstValue(body.code_verifier);
this.#logger.info('Checking the received authorization code for the exchange');
if (!code || !clientId || !redirectUriValue || !codeVerifier) {
throw new oidcErrors.InvalidGrant('Missing required parameters');
}
const granted = this.#grantCodeCache.get(code);
if (!granted) {
throw new oidcErrors.InvalidGrant('Invalid authorization code');
}
// 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) {
await this.#revokeGrantCode(granted, code);
throw new oidcErrors.InvalidGrant('Authorization code has already been used');
}
granted.used = true;
// https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3
if (clientId !== granted.clientId || redirectUriValue !== granted.redirectUri) {
throw new oidcErrors.InvalidGrant('client_id or redirect_uri does not match the authorization code');
}
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
checkPKCE(codeVerifier, granted.codeChallenge, 'S256');
const accessToken = secureRndstr(128);
const now = new Date();
// NOTE: we don't have a setup for automatic token expiration
await this.accessTokensRepository.insert({
id: this.idService.gen(now.getTime()),
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 this.accessTokensRepository.delete({ token: accessToken });
throw new oidcErrors.InvalidGrant('Authorization code has been revoked');
}
granted.grantedToken = accessToken;
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
reply.send({
access_token: accessToken,
token_type: 'Bearer',
scope: granted.scopes.join(' '),
});
} catch (error) {
sendOAuthError(reply, normalizeOAuthError(error));
}
});
}
}

307
pnpm-lock.yaml generated
View File

@@ -102,9 +102,6 @@ importers:
'@fastify/cors':
specifier: 11.2.0
version: 11.2.0
'@fastify/express':
specifier: 4.0.5
version: 4.0.5
'@fastify/http-proxy':
specifier: 11.4.4
version: 11.4.4(bufferutil@4.1.0)(utf-8-validate@6.0.6)
@@ -180,9 +177,6 @@ importers:
blurhash:
specifier: 2.0.5
version: 2.0.5
body-parser:
specifier: 2.2.2
version: 2.2.2
bullmq:
specifier: 5.76.6
version: 5.76.6
@@ -297,12 +291,9 @@ importers:
nsfwjs:
specifier: 4.3.0
version: 4.3.0(@tensorflow/tfjs@4.22.0(seedrandom@3.0.5))(buffer@6.0.3)
oauth2orize:
specifier: 1.12.0
version: 1.12.0
oauth2orize-pkce:
specifier: 0.1.2
version: 0.1.2
oidc-provider:
specifier: 9.8.3
version: 9.8.3
os-utils:
specifier: 0.0.14
version: 0.0.14
@@ -312,9 +303,6 @@ importers:
pg:
specifier: 8.20.0
version: 8.20.0
pkce-challenge:
specifier: 6.0.0
version: 6.0.0
probe-image-size:
specifier: 7.3.0
version: 7.3.0
@@ -415,9 +403,6 @@ importers:
'@types/archiver':
specifier: 7.0.0
version: 7.0.0
'@types/body-parser':
specifier: 1.19.6
version: 1.19.6
'@types/color-convert':
specifier: 3.0.1
version: 3.0.1
@@ -448,12 +433,9 @@ importers:
'@types/nodemailer':
specifier: 8.0.0
version: 8.0.0
'@types/oauth2orize':
specifier: 1.11.5
version: 1.11.5
'@types/oauth2orize-pkce':
specifier: 0.1.2
version: 0.1.2
'@types/oidc-provider':
specifier: 9.5.0
version: 9.5.0
'@types/pg':
specifier: 8.20.0
version: 8.20.0
@@ -532,6 +514,9 @@ importers:
pid-port:
specifier: 2.1.1
version: 2.1.1
pkce-challenge:
specifier: 6.0.0
version: 6.0.0
rolldown:
specifier: 1.0.0
version: 1.0.0
@@ -2166,9 +2151,6 @@ packages:
'@fastify/error@4.2.0':
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
'@fastify/express@4.0.5':
resolution: {integrity: sha512-Sd/6u38XGsOU4ETtw/xwPEZR3cDqEvidL6m+pJQDkvTPPDtXQvcfLZURmbVA1P6uTUhy2HkgV5nFcGatED5+aA==}
'@fastify/fast-json-stringify-compiler@5.0.3':
resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==}
@@ -2475,6 +2457,16 @@ packages:
'@kitajs/html': ^4.2.10
typescript: ^5.9.3
'@koa/cors@5.0.0':
resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==}
engines: {node: '>= 14.0.0'}
'@koa/router@15.5.0':
resolution: {integrity: sha512-KSC0oG/5t6ITu5wqX4lJseA/dngoj14hEaohrLZEXtlUT2RRyJvwaJ0KV+5uQoaWrY3A8ClHOrBEU4g8dujn8Q==}
engines: {node: '>= 20'}
peerDependencies:
koa: ^2.0.0 || ^3.0.0
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
@@ -4262,6 +4254,9 @@ packages:
'@types/cookiejar@2.1.5':
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
'@types/cookies@0.9.2':
resolution: {integrity: sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==}
'@types/debug@4.1.13':
resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
@@ -4307,6 +4302,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/http-assert@1.5.6':
resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==}
'@types/http-cache-semantics@4.2.0':
resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==}
@@ -4331,6 +4329,15 @@ packages:
'@types/jsonld@1.5.15':
resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==}
'@types/keygrip@1.0.6':
resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==}
'@types/koa-compose@3.2.9':
resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==}
'@types/koa@3.0.2':
resolution: {integrity: sha512-7TRzVOBcH/q8CfPh9AmHBQ8TZtimT4Sn+rw8//hXveI6+F41z93W8a+0B0O8L7apKQv+vKBIEZSECiL0Oo1JFA==}
'@types/long@4.0.2':
resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==}
@@ -4373,18 +4380,15 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
'@types/oauth2orize-pkce@0.1.2':
resolution: {integrity: sha512-g5rDzqQTTUIJJpY7UWxb0EU1WyURIwOj3TndKC2krEEEmaKrnZXgoEBkR72QY2kp4cJ6N9cF2AqTPJ0Qyg+caA==}
'@types/oauth2orize@1.11.5':
resolution: {integrity: sha512-C6hrRoh9hCnqis39OpeUZSwgw+TIzcV0CsxwJMGfQjTx4I1r+CLmuEPzoDJr5NRTfc7OMwHNLkQwrGFLKrJjMQ==}
'@types/offscreencanvas@2019.3.0':
resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==}
'@types/offscreencanvas@2019.7.3':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
'@types/oidc-provider@9.5.0':
resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==}
'@types/pg-pool@2.0.7':
resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==}
@@ -5571,6 +5575,10 @@ packages:
constantinople@4.0.1:
resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==}
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
content-disposition@1.1.0:
resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==}
engines: {node: '>=18'}
@@ -5597,6 +5605,10 @@ packages:
cookiejar@2.1.4:
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
cookies@0.9.1:
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
engines: {node: '>= 0.8'}
core-js@3.29.1:
resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==}
@@ -5781,6 +5793,9 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-equal@1.0.1:
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'}
@@ -5823,6 +5838,10 @@ packages:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
depd@1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -5831,6 +5850,10 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
destroy@1.2.0:
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -6174,6 +6197,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
eta@4.6.0:
resolution: {integrity: sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==}
engines: {node: '>=20'}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
@@ -6418,6 +6445,10 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -6665,9 +6696,17 @@ packages:
htmlparser2@9.1.0:
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
http-assert@1.5.0:
resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==}
engines: {node: '>= 0.8'}
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-errors@1.8.1:
resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==}
engines: {node: '>= 0.6'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -7087,6 +7126,9 @@ packages:
resolution: {integrity: sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==}
engines: {node: '>= 20'}
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'}
@@ -7193,6 +7235,10 @@ packages:
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -7203,6 +7249,13 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
koa-compose@4.1.0:
resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==}
koa@3.2.0:
resolution: {integrity: sha512-TrM4/tnNY7uJ1aW55sIIa+dqBvc4V14WRIAlGcWat9wV5pRS9Wr5Zk2ZTjQP1jtfIHDoHiSbPuV08P0fUZo2pg==}
engines: {node: '>= 18'}
ky@1.14.3:
resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==}
engines: {node: '>=18'}
@@ -7933,13 +7986,6 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
oauth2orize-pkce@0.1.2:
resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==}
oauth2orize@1.12.0:
resolution: {integrity: sha512-j4XtFDQUBsvUHPjUmvmNDUDMYed2MphMIJBhyxVVe8hGCjkuYnjIsW+D9qk8c5ciXRdnk6x6tEbiO6PLeOZdCQ==}
engines: {node: '>= 0.4.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -7983,6 +8029,9 @@ packages:
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
oidc-provider@9.8.3:
resolution: {integrity: sha512-YkchaAyVAZbsn/l7IQhcEMdeDL3lwSo/PNUtnsXSqPqT7EG8DRko0EAWzHd/n9VfCtKVkxGjYOY4h4UwFcWnUA==}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
@@ -8643,6 +8692,10 @@ packages:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
quick-lru@7.3.0:
resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==}
engines: {node: '>=18'}
random-seed@0.3.0:
resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==}
engines: {node: '>= 0.6.0'}
@@ -9313,6 +9366,10 @@ packages:
engines: {node: ^22 || >=24}
hasBin: true
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@@ -9723,6 +9780,10 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsscmp@1.0.6:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
@@ -9868,9 +9929,6 @@ packages:
ufo@1.6.4:
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
uid2@0.0.4:
resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==}
uid@2.0.2:
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
engines: {node: '>=8'}
@@ -9973,10 +10031,6 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
@@ -11421,13 +11475,6 @@ snapshots:
'@fastify/error@4.2.0': {}
'@fastify/express@4.0.5':
dependencies:
express: 5.2.1
fastify-plugin: 5.1.0
transitivePeerDependencies:
- supports-color
'@fastify/fast-json-stringify-compiler@5.0.3':
dependencies:
fast-json-stringify: 6.4.0
@@ -11724,6 +11771,20 @@ snapshots:
typescript: 5.9.3
yargs: 18.0.0
'@koa/cors@5.0.0':
dependencies:
vary: 1.1.2
'@koa/router@15.5.0(koa@3.2.0)':
dependencies:
debug: 4.4.3(supports-color@10.2.2)
http-errors: 2.0.1
koa: 3.2.0
koa-compose: 4.1.0
path-to-regexp: 8.4.2
transitivePeerDependencies:
- supports-color
'@kurkle/color@0.3.4': {}
'@levischuck/tiny-cbor@0.2.11': {}
@@ -13733,6 +13794,13 @@ snapshots:
'@types/cookiejar@2.1.5': {}
'@types/cookies@0.9.2':
dependencies:
'@types/connect': 3.4.38
'@types/express': 5.0.6
'@types/keygrip': 1.0.6
'@types/node': 24.12.2
'@types/debug@4.1.13':
dependencies:
'@types/ms': 2.1.0
@@ -13783,6 +13851,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/http-assert@1.5.6': {}
'@types/http-cache-semantics@4.2.0': {}
'@types/http-errors@2.0.5': {}
@@ -13801,6 +13871,23 @@ snapshots:
'@types/jsonld@1.5.15': {}
'@types/keygrip@1.0.6': {}
'@types/koa-compose@3.2.9':
dependencies:
'@types/koa': 3.0.2
'@types/koa@3.0.2':
dependencies:
'@types/accepts': 1.3.7
'@types/content-disposition': 0.5.9
'@types/cookies': 0.9.2
'@types/http-assert': 1.5.6
'@types/http-errors': 2.0.5
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.9
'@types/node': 24.12.2
'@types/long@4.0.2': {}
'@types/matter-js@0.20.2': {}
@@ -13842,19 +13929,16 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
'@types/oauth2orize-pkce@0.1.2':
dependencies:
'@types/oauth2orize': 1.11.5
'@types/oauth2orize@1.11.5':
dependencies:
'@types/express': 5.0.6
'@types/node': 24.12.2
'@types/offscreencanvas@2019.3.0': {}
'@types/offscreencanvas@2019.7.3': {}
'@types/oidc-provider@9.5.0':
dependencies:
'@types/keygrip': 1.0.6
'@types/koa': 3.0.2
'@types/node': 24.12.2
'@types/pg-pool@2.0.7':
dependencies:
'@types/pg': 8.20.0
@@ -15161,6 +15245,8 @@ snapshots:
'@babel/parser': 7.29.3
'@babel/types': 7.29.0
content-disposition@1.0.1: {}
content-disposition@1.1.0: {}
content-type@1.0.5: {}
@@ -15175,6 +15261,11 @@ snapshots:
cookiejar@2.1.4: {}
cookies@0.9.1:
dependencies:
depd: 2.0.0
keygrip: 1.1.0
core-js@3.29.1: {}
core-util-is@1.0.2: {}
@@ -15422,6 +15513,8 @@ snapshots:
deep-eql@5.0.2: {}
deep-equal@1.0.1: {}
deep-equal@2.2.3:
dependencies:
array-buffer-byte-length: 1.0.2
@@ -15470,15 +15563,18 @@ snapshots:
delayed-stream@1.0.0: {}
delegates@1.0.0:
optional: true
delegates@1.0.0: {}
denque@2.1.0: {}
depd@1.1.2: {}
depd@2.0.0: {}
dequal@2.0.3: {}
destroy@1.2.0: {}
detect-libc@2.1.2: {}
devlop@1.1.0:
@@ -15946,6 +16042,8 @@ snapshots:
esutils@2.0.3: {}
eta@4.6.0: {}
etag@1.8.1: {}
event-target-shim@5.0.1: {}
@@ -16293,6 +16391,8 @@ snapshots:
forwarded@0.2.0: {}
fresh@0.5.2: {}
fresh@2.0.0: {}
fs-extra@11.3.5:
@@ -16597,8 +16697,21 @@ snapshots:
domutils: 3.2.2
entities: 4.5.0
http-assert@1.5.0:
dependencies:
deep-equal: 1.0.1
http-errors: 1.8.1
http-cache-semantics@4.2.0: {}
http-errors@1.8.1:
dependencies:
depd: 1.1.2
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 1.5.0
toidentifier: 1.0.1
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -17000,6 +17113,8 @@ snapshots:
'@hapi/topo': 6.0.2
'@standard-schema/spec': 1.1.0
jose@6.2.3: {}
js-beautify@1.15.4:
dependencies:
config-chain: 1.1.13
@@ -17108,6 +17223,10 @@ snapshots:
jwa: 2.0.1
safe-buffer: 5.2.1
keygrip@1.1.0:
dependencies:
tsscmp: 1.0.6
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -17118,6 +17237,29 @@ snapshots:
kind-of@6.0.3: {}
koa-compose@4.1.0: {}
koa@3.2.0:
dependencies:
accepts: 1.3.8
content-disposition: 1.0.1
content-type: 1.0.5
cookies: 0.9.1
delegates: 1.0.0
destroy: 1.2.0
encodeurl: 2.0.0
escape-html: 1.0.3
fresh: 0.5.2
http-assert: 1.5.0
http-errors: 2.0.1
koa-compose: 4.1.0
mime-types: 3.0.2
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
type-is: 2.0.1
vary: 1.1.2
ky@1.14.3: {}
launder@1.7.1:
@@ -17986,16 +18128,6 @@ snapshots:
dependencies:
boolbase: 1.0.0
oauth2orize-pkce@0.1.2: {}
oauth2orize@1.12.0:
dependencies:
debug: 2.6.9
uid2: 0.0.4
utils-merge: 1.0.1
transitivePeerDependencies:
- supports-color
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -18047,6 +18179,21 @@ snapshots:
obug@2.1.1: {}
oidc-provider@9.8.3:
dependencies:
'@koa/cors': 5.0.0
'@koa/router': 15.5.0(koa@3.2.0)
debug: 4.4.3(supports-color@10.2.2)
eta: 4.6.0
jose: 6.2.3
jsesc: 3.1.0
koa: 3.2.0
nanoid: 5.1.11
quick-lru: 7.3.0
raw-body: 3.0.2
transitivePeerDependencies:
- supports-color
on-exit-leak-free@2.1.2: {}
on-finished@2.4.1:
@@ -18686,6 +18833,8 @@ snapshots:
quick-lru@5.1.1: {}
quick-lru@7.3.0: {}
random-seed@0.3.0:
dependencies:
json-stringify-safe: 5.0.1
@@ -19491,6 +19640,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
statuses@1.5.0: {}
statuses@2.0.2: {}
std-env@4.1.0: {}
@@ -19924,6 +20075,8 @@ snapshots:
tslib@2.8.1: {}
tsscmp@1.0.6: {}
tsx@4.21.0:
dependencies:
esbuild: 0.27.7
@@ -20039,8 +20192,6 @@ snapshots:
ufo@1.6.4: {}
uid2@0.0.4: {}
uid@2.0.2:
dependencies:
'@lukeed/csprng': 1.1.0
@@ -20144,8 +20295,6 @@ snapshots:
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
uuid@11.1.0: {}
uuid@11.1.1: {}