From 865465053e37b56d1d246a855204967531c6bbe2 Mon Sep 17 00:00:00 2001 From: duanhongyi Date: Wed, 22 Apr 2026 17:51:37 +0800 Subject: [PATCH] feat(architecture): enhance compatibility --- lib/oauth/http.js | 46 ++++ lib/oauth/id-token.js | 22 ++ lib/providers/apple.js | 21 ++ lib/providers/base.js | 50 ++++ lib/providers/bitbucket.js | 38 +++ lib/providers/discord.js | 34 +++ lib/providers/facebook.js | 31 +++ lib/providers/github.js | 52 ++++ lib/providers/gitlab.js | 49 ++++ lib/providers/google.js | 32 +++ lib/providers/index.js | 54 ++++ lib/providers/linkedin.js | 32 +++ lib/providers/microsoft.js | 33 +++ lib/providers/twitter.js | 29 ++ library.js | 363 ++++++++++++------------- package-lock.json | 96 ++++--- package.json | 1 + yarn.lock | 527 ++++++++++++++++++++++++++++++++++++- 18 files changed, 1261 insertions(+), 249 deletions(-) create mode 100644 lib/oauth/http.js create mode 100644 lib/oauth/id-token.js create mode 100644 lib/providers/apple.js create mode 100644 lib/providers/base.js create mode 100644 lib/providers/bitbucket.js create mode 100644 lib/providers/discord.js create mode 100644 lib/providers/facebook.js create mode 100644 lib/providers/github.js create mode 100644 lib/providers/gitlab.js create mode 100644 lib/providers/google.js create mode 100644 lib/providers/index.js create mode 100644 lib/providers/linkedin.js create mode 100644 lib/providers/microsoft.js create mode 100644 lib/providers/twitter.js diff --git a/lib/oauth/http.js b/lib/oauth/http.js new file mode 100644 index 0000000..c154b5d --- /dev/null +++ b/lib/oauth/http.js @@ -0,0 +1,46 @@ +'use strict'; + +const https = require('https'); +const { URL } = require('url'); + +function hostOf(u) { + try { + return new URL(u).host.toLowerCase(); + } catch { + return ''; + } +} + +function httpsGetJson({ url, accessToken, headers = {} }) { + return new Promise((resolve, reject) => { + const u = new URL(url); + const req = https.request({ + method: 'GET', + hostname: u.hostname, + port: u.port || 443, + path: `${u.pathname}${u.search || ''}`, + headers: Object.assign({ + 'User-Agent': 'nodebb-plugin-sso-oauth2-multiple', + 'Authorization': `Bearer ${accessToken}`, + }, headers), + }, (res) => { + let body = ''; + res.on('data', (c) => { body += c; }); + res.on('end', () => { + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject(new Error(`${url} -> ${res.statusCode}: ${body.slice(0, 300)}`)); + } + try { + resolve(body ? JSON.parse(body) : null); + } catch (e) { + reject(e); + } + }); + }); + req.on('error', reject); + req.end(); + }); +} + +exports.hostOf = hostOf; +exports.httpsGetJson = httpsGetJson; diff --git a/lib/oauth/id-token.js b/lib/oauth/id-token.js new file mode 100644 index 0000000..c746824 --- /dev/null +++ b/lib/oauth/id-token.js @@ -0,0 +1,22 @@ +'use strict'; + +const winston = require.main.require('winston'); + +// Parses the payload section of a JWT without verifying the signature. The +// resulting object exposes the id_token claims (sub, email, name, ...). +function extractIdTokenClaims(idToken) { + if (!idToken || typeof idToken !== 'string') return null; + const parts = idToken.split('.'); + if (parts.length < 2) return null; + try { + const payload = Buffer + .from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64') + .toString('utf8'); + return JSON.parse(payload); + } catch (e) { + winston.warn(`[sso-oauth2-multiple] id_token parse failed: ${e.message}`); + return null; + } +} + +exports.extractIdTokenClaims = extractIdTokenClaims; diff --git a/lib/providers/apple.js b/lib/providers/apple.js new file mode 100644 index 0000000..790fcc6 --- /dev/null +++ b/lib/providers/apple.js @@ -0,0 +1,21 @@ +'use strict'; + +// Apple has no userinfo endpoint; profile data comes exclusively from the +// id_token claims, which are merged in by the caller. +module.exports = { + type: 'apple', + pkceDefault: true, + skipUserRoute: true, + + matches({ host, name }) { + return /appleid\.apple\.com/.test(host) || /apple/.test(name); + }, + + parse() { + return {}; + }, + + async fetchEmail() { + return null; + }, +}; diff --git a/lib/providers/base.js b/lib/providers/base.js new file mode 100644 index 0000000..83618f5 --- /dev/null +++ b/lib/providers/base.js @@ -0,0 +1,50 @@ +'use strict'; + +// Default provider: generic OIDC / unknown OAuth2. Also acts as the fallback +// base for providers that only override a subset of hooks. +// +// Every provider module exports the following shape (all fields optional +// except `type` and `parse`): +// +// { +// type: 'github', // lowercase identifier +// pkceDefault: false, // force PKCE on by default +// skipUserRoute: false, // skip userinfo HTTP call +// matches({ host, name, profile }) -> bool, // auto-detection heuristic +// userRouteHeaders() -> object, // extra headers for userinfo +// parse(rawProfile, { idKey }) -> normalized,// shape the profile +// fetchEmail({ strategyConfig, accessToken }) -> { email, verified } | null +// } + +module.exports = { + type: 'oidc', + + // The generic provider is the fallback; it never claims a match itself. + matches() { + return false; + }, + + parse(profile, { idKey } = {}) { + const { + id, sub, + name, nickname, preferred_username: preferredUsername, login, + given_name: givenName, middle_name: middleName, family_name: familyName, + picture, avatar_url: avatarUrl, email, email_verified: emailVerified, + } = profile; + const displayName = nickname || preferredUsername || login || name; + const combined = [givenName, middleName, familyName].filter(Boolean).join(' '); + return { + id: (idKey && profile[idKey]) || id || sub, + displayName, + fullname: name || combined || displayName, + picture: picture || avatarUrl, + email, + email_verified: emailVerified, + username: login || preferredUsername || nickname, + }; + }, + + async fetchEmail() { + return null; + }, +}; diff --git a/lib/providers/bitbucket.js b/lib/providers/bitbucket.js new file mode 100644 index 0000000..7cbdf98 --- /dev/null +++ b/lib/providers/bitbucket.js @@ -0,0 +1,38 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +function pickBitbucketEmail(payload) { + const list = payload && Array.isArray(payload.values) ? payload.values : null; + if (!list) return null; + const p = list.find(i => i.is_primary && i.is_confirmed) || + list.find(i => i.is_primary) || + list.find(i => i.is_confirmed) || + list[0]; + return p ? { email: p.email, verified: !!p.is_confirmed } : null; +} + +module.exports = { + type: 'bitbucket', + + matches({ host, name }) { + return host === 'api.bitbucket.org' || /bitbucket/.test(name); + }, + + parse(profile) { + return { + id: profile.uuid || profile.account_id || profile.username, + displayName: profile.username || profile.display_name, + fullname: profile.display_name || profile.username, + picture: profile.links && profile.links.avatar && profile.links.avatar.href, + username: profile.username, + }; + }, + + async fetchEmail({ accessToken }) { + return pickBitbucketEmail(await httpsGetJson({ + url: 'https://api.bitbucket.org/2.0/user/emails', + accessToken, + })); + }, +}; diff --git a/lib/providers/discord.js b/lib/providers/discord.js new file mode 100644 index 0000000..6616371 --- /dev/null +++ b/lib/providers/discord.js @@ -0,0 +1,34 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +module.exports = { + type: 'discord', + + matches({ host, name }) { + return host === 'discord.com' || host === 'discordapp.com' || /discord/.test(name); + }, + + parse(profile) { + const avatar = profile.avatar ? + `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` : + undefined; + return { + id: profile.id, + displayName: profile.global_name || profile.username, + fullname: profile.global_name || profile.username, + picture: avatar, + email: profile.email || undefined, + email_verified: typeof profile.verified === 'boolean' ? profile.verified : undefined, + username: profile.username, + }; + }, + + async fetchEmail({ accessToken }) { + const json = await httpsGetJson({ + url: 'https://discord.com/api/users/@me', + accessToken, + }); + return json && json.email ? { email: json.email, verified: !!json.verified } : null; + }, +}; diff --git a/lib/providers/facebook.js b/lib/providers/facebook.js new file mode 100644 index 0000000..9cb4c28 --- /dev/null +++ b/lib/providers/facebook.js @@ -0,0 +1,31 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +module.exports = { + type: 'facebook', + + matches({ host, name }) { + return host === 'graph.facebook.com' || /facebook|meta/.test(name); + }, + + parse(profile) { + const picture = profile.picture && profile.picture.data && profile.picture.data.url; + return { + id: profile.id, + displayName: profile.name, + fullname: profile.name, + picture, + email: profile.email || undefined, + email_verified: profile.email ? true : undefined, + }; + }, + + async fetchEmail({ accessToken }) { + const json = await httpsGetJson({ + url: 'https://graph.facebook.com/v19.0/me?fields=id,email', + accessToken, + }); + return json && json.email ? { email: json.email, verified: true } : null; + }, +}; diff --git a/lib/providers/github.js b/lib/providers/github.js new file mode 100644 index 0000000..c637fb4 --- /dev/null +++ b/lib/providers/github.js @@ -0,0 +1,52 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +const GITHUB_USER_HEADERS = { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', +}; + +function pickGitHubEmail(list) { + if (!Array.isArray(list)) return null; + const p = list.find(i => i.primary && i.verified) || + list.find(i => i.primary) || + list.find(i => i.verified) || + list[0]; + return p ? { email: p.email, verified: !!p.verified } : null; +} + +module.exports = { + type: 'github', + + matches({ host, name, profile }) { + if (host === 'api.github.com' || /github/.test(name)) return true; + if (profile && (profile.login !== undefined || profile.html_url || profile.gravatar_id)) { + return true; + } + return false; + }, + + userRouteHeaders() { + return GITHUB_USER_HEADERS; + }, + + parse(profile) { + return { + id: profile.id, + displayName: profile.login || profile.name, + fullname: profile.name || profile.login, + picture: profile.avatar_url, + email: profile.email || undefined, + username: profile.login, + }; + }, + + async fetchEmail({ accessToken }) { + return pickGitHubEmail(await httpsGetJson({ + url: 'https://api.github.com/user/emails', + accessToken, + headers: GITHUB_USER_HEADERS, + })); + }, +}; diff --git a/lib/providers/gitlab.js b/lib/providers/gitlab.js new file mode 100644 index 0000000..949b688 --- /dev/null +++ b/lib/providers/gitlab.js @@ -0,0 +1,49 @@ +'use strict'; + +const { URL } = require('url'); +const { httpsGetJson } = require('../oauth/http'); + +function gitlabApiBase(userRoute) { + try { + const u = new URL(userRoute); + const m = u.pathname.match(/^(.*\/api\/v4)\b/); + return `${u.protocol}//${u.host}${m ? m[1] : '/api/v4'}`; + } catch { + return 'https://gitlab.com/api/v4'; + } +} + +module.exports = { + type: 'gitlab', + + matches({ profile }, strategyConfig) { + if (strategyConfig && strategyConfig.userRoute && /\/api\/v4\/user\b/.test(strategyConfig.userRoute)) { + return true; + } + if (profile && (profile.web_url || profile.avatar_url) && + profile.username && profile.id && profile.state) { + return true; + } + return false; + }, + + parse(profile) { + return { + id: profile.id, + displayName: profile.username || profile.name, + fullname: profile.name || profile.username, + picture: profile.avatar_url, + email: profile.email || profile.public_email || undefined, + email_verified: profile.confirmed_at ? true : undefined, + username: profile.username, + }; + }, + + async fetchEmail({ strategyConfig, accessToken }) { + const base = gitlabApiBase(strategyConfig.userRoute); + const json = await httpsGetJson({ url: `${base}/user`, accessToken }); + return json && json.email ? { email: json.email, verified: !!json.confirmed_at } : null; + }, +}; + +module.exports.gitlabApiBase = gitlabApiBase; diff --git a/lib/providers/google.js b/lib/providers/google.js new file mode 100644 index 0000000..b375244 --- /dev/null +++ b/lib/providers/google.js @@ -0,0 +1,32 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +module.exports = { + type: 'google', + + matches({ host, name }) { + if (host === 'openidconnect.googleapis.com' || host === 'www.googleapis.com') return true; + return /google/.test(name); + }, + + // /v1/userinfo: { sub, name, given_name, family_name, picture, email, email_verified, locale } + parse(profile) { + return { + id: profile.sub, + displayName: profile.name || (profile.email ? profile.email.split('@')[0] : undefined), + fullname: profile.name, + picture: profile.picture, + email: profile.email, + email_verified: profile.email_verified, + }; + }, + + async fetchEmail({ accessToken }) { + const json = await httpsGetJson({ + url: 'https://openidconnect.googleapis.com/v1/userinfo', + accessToken, + }); + return json && json.email ? { email: json.email, verified: !!json.email_verified } : null; + }, +}; diff --git a/lib/providers/index.js b/lib/providers/index.js new file mode 100644 index 0000000..fffb9e5 --- /dev/null +++ b/lib/providers/index.js @@ -0,0 +1,54 @@ +'use strict'; + +// Provider registry. Each provider module declares its own detection +// heuristics, profile parser, and email fallback. Adding a new provider +// means dropping a new file into this folder and listing it here. + +const { hostOf } = require('../oauth/http'); + +const base = require('./base'); + +const providers = [ + require('./github'), + require('./bitbucket'), + require('./linkedin'), + require('./discord'), + require('./facebook'), + require('./twitter'), + require('./apple'), + require('./google'), + require('./microsoft'), + require('./gitlab'), +]; + +const byType = new Map(providers.map(p => [p.type, p])); + +function get(type) { + if (!type) return base; + return byType.get(String(type).toLowerCase()) || base; +} + +function detect(strategyConfig, rawProfile) { + const explicit = ((strategyConfig && strategyConfig.providerType) || '').trim().toLowerCase(); + if (explicit) return get(explicit); + + const host = hostOf((strategyConfig && strategyConfig.userRoute) || + (strategyConfig && strategyConfig.authUrl) || ''); + const name = ((strategyConfig && strategyConfig.name) || '').toLowerCase(); + const ctx = { host, name, profile: rawProfile }; + + for (const p of providers) { + try { + // Pass strategyConfig as a second arg so providers like gitlab can + // inspect the configured userRoute when it is not reflected in the + // probe context. + if (p.matches && p.matches(ctx, strategyConfig)) return p; + } catch (_) { /* ignore individual matcher errors */ } + } + return base; +} + +exports.base = base; +exports.get = get; +exports.detect = detect; +exports.all = providers; diff --git a/lib/providers/linkedin.js b/lib/providers/linkedin.js new file mode 100644 index 0000000..60d6ad0 --- /dev/null +++ b/lib/providers/linkedin.js @@ -0,0 +1,32 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +module.exports = { + type: 'linkedin', + + matches({ host, name }) { + return host === 'api.linkedin.com' || /linkedin/.test(name); + }, + + parse(profile) { + const fullname = [profile.localizedFirstName, profile.localizedLastName] + .filter(Boolean) + .join(' '); + return { + id: profile.id, + displayName: fullname || profile.id, + fullname, + }; + }, + + async fetchEmail({ accessToken }) { + const json = await httpsGetJson({ + url: 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', + accessToken, + }); + const el = json && Array.isArray(json.elements) ? json.elements[0] : null; + const email = el && el['handle~'] && el['handle~'].emailAddress; + return email ? { email, verified: true } : null; + }, +}; diff --git a/lib/providers/microsoft.js b/lib/providers/microsoft.js new file mode 100644 index 0000000..942fc91 --- /dev/null +++ b/lib/providers/microsoft.js @@ -0,0 +1,33 @@ +'use strict'; + +const { httpsGetJson } = require('../oauth/http'); + +module.exports = { + type: 'microsoft', + + matches({ host, name }) { + if (/graph\.microsoft\.com|login\.microsoftonline\.com/.test(host)) return true; + return /microsoft|azure|entra/.test(name); + }, + + // Graph /me: { id, displayName, userPrincipalName, mail, givenName, surname } + parse(profile) { + const email = profile.mail || profile.userPrincipalName; + return { + id: profile.id, + displayName: profile.displayName || email, + fullname: profile.displayName || [profile.givenName, profile.surname].filter(Boolean).join(' '), + email, + email_verified: email ? true : undefined, + }; + }, + + async fetchEmail({ accessToken }) { + const json = await httpsGetJson({ + url: 'https://graph.microsoft.com/v1.0/me', + accessToken, + }); + const email = json && (json.mail || json.userPrincipalName); + return email ? { email, verified: true } : null; + }, +}; diff --git a/lib/providers/twitter.js b/lib/providers/twitter.js new file mode 100644 index 0000000..61b0965 --- /dev/null +++ b/lib/providers/twitter.js @@ -0,0 +1,29 @@ +'use strict'; + +const winston = require.main.require('winston'); + +module.exports = { + type: 'twitter', + pkceDefault: true, + + matches({ host, name }) { + if (host === 'api.twitter.com' || host === 'api.x.com') return true; + return /twitter|^x$|\bx\b/.test(name); + }, + + parse(profile) { + const d = profile.data || profile; + return { + id: d.id, + displayName: d.username || d.name, + fullname: d.name || d.username, + picture: d.profile_image_url, + username: d.username, + }; + }, + + async fetchEmail() { + winston.warn('[sso-oauth2-multiple] twitter: email cannot be fetched via OAuth2; skipping'); + return null; + }, +}; diff --git a/library.js b/library.js index 0161c03..95a1b22 100644 --- a/library.js +++ b/library.js @@ -1,9 +1,7 @@ 'use strict'; - const passport = module.parent.require('passport'); const nconf = module.parent.require('nconf'); const winston = module.parent.require('winston'); - const db = require.main.require('./src/database'); const user = require.main.require('./src/user'); const plugins = require.main.require('./src/plugins'); @@ -11,115 +9,126 @@ const meta = require.main.require('./src/meta'); const groups = require.main.require('./src/groups'); const authenticationController = require.main.require('./src/controllers/authentication'); const routeHelpers = require.main.require('./src/routes/helpers'); - +const providers = require('./lib/providers'); +const { httpsGetJson } = require('./lib/oauth/http'); +const { extractIdTokenClaims } = require('./lib/oauth/id-token'); const OAuth = module.exports; - +// --------------------------------------------------------------------------- +// Admin / routes +// --------------------------------------------------------------------------- OAuth.init = async (params) => { - const { router /* , middleware , controllers */ } = params; + const { router } = params; const controllers = require('./lib/controllers'); - routeHelpers.setupAdminPageRoute(router, '/admin/plugins/sso-oauth2-multiple', controllers.renderAdminPage); }; - OAuth.addRoutes = async ({ router, middleware }) => { const controllers = require('./lib/controllers'); - const middlewares = [ - middleware.ensureLoggedIn, - middleware.admin.checkPrivileges, - ]; - + const middlewares = [middleware.ensureLoggedIn, middleware.admin.checkPrivileges]; routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/discover', middlewares, controllers.getOpenIdMetadata); - routeHelpers.setupApiRoute(router, 'post', '/oauth2-multiple/strategies', middlewares, controllers.editStrategy); routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/strategies/:name', middlewares, controllers.getStrategy); routeHelpers.setupApiRoute(router, 'delete', '/oauth2-multiple/strategies/:name', middlewares, controllers.deleteStrategy); - routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/provider/:provider/user/:oAuthId', middlewares, controllers.userByOAuthId); }; - OAuth.addAdminNavigation = (header) => { header.authentication.push({ route: '/plugins/sso-oauth2-multiple', icon: 'fa-tint', name: 'Multiple OAuth2', }); - return header; }; - +// --------------------------------------------------------------------------- +// Strategy storage +// --------------------------------------------------------------------------- OAuth.listStrategies = async (full) => { const names = await db.getSortedSetMembers('oauth2-multiple:strategies'); names.sort(); - return await getStrategies(names, full); }; - OAuth.getStrategy = async (name) => { const strategies = await getStrategies([name], true); return strategies.length ? strategies[0] : null; }; - async function getStrategies(names, full) { - const strategies = await db.getObjects(names.map(name => `oauth2-multiple:strategies:${name}`), full ? undefined : ['enabled']); + const strategies = await db.getObjects( + names.map(name => `oauth2-multiple:strategies:${name}`), + full ? undefined : ['enabled'] + ); strategies.forEach((strategy, idx) => { strategy.name = names[idx]; strategy.enabled = strategy.enabled === 'true' || strategy.enabled === true; strategy.callbackUrl = `${nconf.get('url')}/auth/${names[idx]}/callback`; }); - return strategies; } - +// --------------------------------------------------------------------------- +// Passport wiring +// --------------------------------------------------------------------------- +// We use `passport-oauth2` (not `passport-oauth`) because it supports PKCE/state, +// which are required for Twitter/X and recommended for Apple/Google/Microsoft. +const OAuth2Strategy = require('passport-oauth2'); OAuth.loadStrategies = async (strategies) => { - const passportOAuth = require('passport-oauth').OAuth2Strategy; - let configured = await OAuth.listStrategies(true); configured = configured.filter(obj => obj.enabled); - - const configs = configured.map(({ - name, - authUrl: authorizationURL, - tokenUrl: tokenURL, - id: clientID, - secret: clientSecret, - callbackUrl: callbackURL, - }) => new passportOAuth({ - authorizationURL, - tokenURL, - clientID, - clientSecret, - callbackURL, - passReqToCallback: true, - }, async (req, token, secret, profile, done) => { - const { id, displayName, email, email_verified } = profile; - if (![id, displayName, email].every(Boolean)) { - return done(new Error('insufficient-scope')); + configured.forEach((cfg) => { + const provider = providers.detect(cfg); + // PKCE / state are opt-in (off by default) to preserve backward compatibility + // with v1.5.x (which used `passport-oauth` and did neither). Some providers + // (twitter/apple) recommend PKCE; users can enable it explicitly in ACP. + // Note: passport-oauth2 requires `state: true` whenever `pkce: true`. + const pkce = cfg.pkce === 'on' || cfg.pkce === true; + const stateEnabled = pkce || cfg.state === 'on' || cfg.state === true; + if (!pkce && provider.pkceDefault) { + winston.verbose(`[sso-oauth2-multiple] ${cfg.name}: provider ${provider.type} recommends PKCE; enable it in ACP if your IdP requires it.`); } - try { - const user = await OAuth.login({ - name, - oAuthid: id, - handle: displayName, - email, - email_verified, + const strategyOptions = { + authorizationURL: cfg.authUrl, + tokenURL: cfg.tokenUrl, + clientID: cfg.id, + clientSecret: cfg.secret, + callbackURL: cfg.callbackUrl, + passReqToCallback: true, + state: stateEnabled, + pkce, + scope: cfg.scope ? cfg.scope.split(/\s+/) : ['openid', 'email', 'profile'], + }; + const strategy = new OAuth2Strategy(strategyOptions, + async (req, accessToken, refreshToken, params, profile, done) => { + try { + // `profile` from passport-oauth2 is {} by default; we build our own. + const idTokenClaims = extractIdTokenClaims(params && params.id_token); + const built = await OAuth.buildProfile({ + strategyConfig: cfg, + accessToken, + idTokenClaims, + }); + const id = built && built.id; + const displayName = built && (built.displayName || built.fullname || built.username); + winston.info(`[sso-oauth2-multiple] verify ${cfg.name}: id=${id} displayName=${displayName} email=${built.email || 'none'} verified=${built.email_verified}`); + if (!id || !displayName) { + return done(new Error('insufficient-scope')); + } + const loggedInUser = await OAuth.login({ + name: cfg.name, + oAuthid: id, + handle: displayName, + email: built.email, + email_verified: built.email_verified, + }); + await authenticationController.onSuccessfulLogin(req, loggedInUser.uid); + await OAuth.assignGroups({ provider: cfg.name, user: loggedInUser, profile: built }); + await OAuth.updateProfile(loggedInUser.uid, built); + done(null, loggedInUser); + plugins.hooks.fire('action:oauth2.login', { name: cfg.name, user: loggedInUser, profile: built }); + } catch (err) { + done(err); + } }); - winston.verbose(`[plugin/sso-oauth2-multiple] Successful login to uid ${user.uid} via ${name} (remote id ${id})`); - await authenticationController.onSuccessfulLogin(req, user.uid); - await OAuth.assignGroups({ provider: name, user, profile }); - await OAuth.updateProfile(user.uid, profile); - done(null, user); - - plugins.hooks.fire('action:oauth2.login', { name, user, profile }); - } catch (err) { - done(err); - } - })); - - configs.forEach((strategy, idx) => { - strategy.userProfile = OAuth.getUserProfile.bind(strategy, configured[idx].name, configured[idx].userRoute); - passport.use(configured[idx].name, strategy); + // Override passport-oauth2 userProfile to no-op; we do it ourselves in verify + strategy.userProfile = (_accessToken, done) => done(null, {}); + passport.use(cfg.name, strategy); }); - strategies.push(...configured.map(({ name, scope, loginLabel, registerLabel, faIcon }) => ({ name, url: `/auth/${name}`, @@ -136,193 +145,161 @@ OAuth.loadStrategies = async (strategies) => { color: '#666', scope: scope || 'openid email profile', }))); - return strategies; }; - -OAuth.getUserProfile = function (name, userRoute, accessToken, done) { - // If your OAuth provider requires the access token to be sent in the query parameters - // instead of the request headers, comment out the next line: - this._oauth2._useAuthorizationHeaderForGET = true; - - this._oauth2.get(userRoute, accessToken, async (err, body/* , res */) => { - if (err) { - return done(err); - } - +// --------------------------------------------------------------------------- +// Build a normalized profile from: userinfo endpoint + id_token + per-provider +// email fallback +// --------------------------------------------------------------------------- +OAuth.buildProfile = async ({ strategyConfig, accessToken, idTokenClaims }) => { + // First detection pass (pre-userinfo) uses only the strategy config. + let provider = providers.detect(strategyConfig, idTokenClaims || {}); + let raw = {}; + const hasUserRoute = strategyConfig.userRoute && !provider.skipUserRoute; + if (hasUserRoute) { try { - const json = JSON.parse(body); - const profile = await OAuth.parseUserReturn(name, json); - profile.provider = name; - done(null, profile); + const extraHeaders = provider.userRouteHeaders ? provider.userRouteHeaders() : null; + raw = await httpsGetJson({ + url: strategyConfig.userRoute, + accessToken, + headers: extraHeaders || { 'Accept': 'application/json' }, + }) || {}; } catch (e) { - done(e); + winston.warn(`[sso-oauth2-multiple] ${strategyConfig.name}: userRoute error: ${e.message}`); } - }); + } + // Re-detect now that we have the raw profile; this lets heuristics that + // inspect profile shape (e.g. gitlab / github fallback) kick in. + if (!(strategyConfig.providerType || '').trim()) { + provider = providers.detect(strategyConfig, raw); + } + winston.info(`[sso-oauth2-multiple] ${strategyConfig.name}: providerType=${provider.type} userinfoKeys=${Object.keys(raw).join(',') || '-'} idTokenKeys=${idTokenClaims ? Object.keys(idTokenClaims).join(',') : '-'}`); + const profile = await OAuth.parseUserReturn(strategyConfig.name, raw, provider.type); + // Merge id_token claims (OIDC) as higher priority for email/name if userinfo lacks them + if (idTokenClaims) { + if (!profile.email && idTokenClaims.email) profile.email = idTokenClaims.email; + if (typeof profile.email_verified === 'undefined' && typeof idTokenClaims.email_verified !== 'undefined') { + profile.email_verified = !!idTokenClaims.email_verified; + } + if (!profile.id && (idTokenClaims.sub || idTokenClaims.user_id)) { + profile.id = idTokenClaims.sub || idTokenClaims.user_id; + } + if (!profile.fullname && idTokenClaims.name) profile.fullname = idTokenClaims.name; + if (!profile.picture && idTokenClaims.picture) profile.picture = idTokenClaims.picture; + if (!profile.displayName) { + profile.displayName = idTokenClaims.preferred_username || idTokenClaims.nickname || idTokenClaims.name || (idTokenClaims.email ? idTokenClaims.email.split('@')[0] : undefined); + } + } + // Provider-specific REST fallback for email + if (!profile.email && provider.fetchEmail) { + try { + const fetched = await provider.fetchEmail({ strategyConfig, accessToken }); + if (fetched && fetched.email) { + profile.email = fetched.email; + if (typeof profile.email_verified === 'undefined') profile.email_verified = !!fetched.verified; + winston.info(`[sso-oauth2-multiple] ${strategyConfig.name}: fetched email via ${provider.type} fallback`); + } + } catch (emailErr) { + winston.warn(`[sso-oauth2-multiple] ${strategyConfig.name}: email fallback failed: ${emailErr.message}`); + } + } + // Apple last resort: synthesize displayName from email + if (!profile.displayName && profile.email) { + profile.displayName = profile.email.split('@')[0]; + } + profile.provider = strategyConfig.name; + return profile; }; - -OAuth.parseUserReturn = async (provider, profile) => { - const { - id, sub, - name, nickname, preferred_username, - given_name, middle_name, family_name, - picture, roles, email, email_verified, - } = profile; - const { usernameViaEmail, forceUsernameViaEmail, idKey } = await OAuth.getStrategy(provider); - - const displayName = nickname || preferred_username || name; - - const combinedFullName = [given_name, middle_name, family_name].filter(Boolean).join(' '); - const fullname = name || combinedFullName; - - const normalized = { - provider, - id: profile[idKey] || id || sub, - displayName, - fullname, - picture, - roles, - email, - email_verified, - }; - - if (forceUsernameViaEmail || (!normalized.displayName && email && usernameViaEmail === 'on')) { - normalized.displayName = email.split('@')[0]; +// --------------------------------------------------------------------------- +// Profile normalization +// --------------------------------------------------------------------------- +OAuth.parseUserReturn = async (strategyName, rawProfile, providerType) => { + const strategy = await OAuth.getStrategy(strategyName); + const { usernameViaEmail, forceUsernameViaEmail, idKey } = strategy || {}; + + const provider = providers.get(providerType); + const parsed = provider.parse(rawProfile, { idKey }); + + // Common post-processing: create username from email if needed + if (forceUsernameViaEmail || (!parsed.displayName && parsed.email && usernameViaEmail === 'on')) { + parsed.displayName = parsed.email.split('@')[0]; } - return normalized; + parsed.provider = strategyName; + parsed.roles = rawProfile.roles; + return parsed; }; - +// --------------------------------------------------------------------------- +// User login / linking +// --------------------------------------------------------------------------- OAuth.getAssociations = async () => { let { roles, groups } = await meta.settings.get('sso-oauth2-multiple'); - if (!roles || !groups) { - return []; - } - - if (!Array.isArray(groups)) { - groups = groups.split(','); - } - if (!Array.isArray(roles)) { - roles = roles.split(','); - } - return roles.map((role, idx) => ({ - role, - group: groups[idx], - })); + if (!roles || !groups) return []; + if (!Array.isArray(groups)) groups = groups.split(','); + if (!Array.isArray(roles)) roles = roles.split(','); + return roles.map((role, idx) => ({ role, group: groups[idx] })); }; - OAuth.login = async (payload) => { let uid = await OAuth.getUidByOAuthid(payload.name, payload.oAuthid); - if (uid !== null) { - // Existing User - return ({ uid }); - } - + if (uid !== null) return { uid }; const { trustEmailVerified } = await OAuth.getStrategy(payload.name); const { email } = payload; - const email_verified = - parseInt(trustEmailVerified, 10) && + const email_verified = parseInt(trustEmailVerified, 10) && (payload.email_verified || payload.email_verified === true); - - - // Check for user via email fallback - if (email && email_verified) { - uid = await user.getUidByEmail(payload.email); - } - + if (email && email_verified) uid = await user.getUidByEmail(payload.email); if (!uid) { - // New user - uid = await user.create({ - username: payload.handle, - }); - - // Automatically confirm user email + uid = await user.create({ username: payload.handle }); if (email) { await user.setUserField(uid, 'email', email); - - if (email_verified) { - await user.email.confirmByUid(uid); - } + if (email_verified) await user.email.confirmByUid(uid); } } - - // Save provider-specific information to the user await user.setUserField(uid, `${payload.name}Id`, payload.oAuthid); await db.setObjectField(`${payload.name}Id:uid`, payload.oAuthid, uid); - return { uid }; }; - OAuth.assignGroups = async ({ user, profile }) => { - if (!profile.roles || !Array.isArray(profile.roles)) { - return; - } - + if (!profile.roles || !Array.isArray(profile.roles)) return; const { uid } = user; const associations = await OAuth.getAssociations(); const { toJoin, toLeave } = associations.reduce((memo, { role, group }) => { - if (profile.roles.includes(role)) { - memo.toJoin.push(group); - } else { - memo.toLeave.push(group); - } - + if (profile.roles.includes(role)) memo.toJoin.push(group); + else memo.toLeave.push(group); return memo; }, { toJoin: [], toLeave: [] }); - if (toLeave.length) { - winston.verbose(`[plugins/sso-auth0] uid ${uid} now leaving ${toLeave.length} these user groups: ${toLeave.join(', ')}`); - } await groups.leave(toLeave, uid); await groups.join(toJoin, uid); - winston.verbose(`[plugins/sso-auth0] uid ${uid} now a part of ${toJoin.length} these user groups: ${toJoin.join(', ')}`); }; - OAuth.updateProfile = async (uid, profile) => { const fields = ['fullname', 'picture']; const strategy = await OAuth.getStrategy(profile.provider); const allowList = []; - const payload = fields.reduce((memo, field) => { const setting = `sync${field[0].toUpperCase()}${field.slice(1)}`; if (strategy[setting] && parseInt(strategy[setting], 10)) { memo[field] = profile[field]; - if (field === 'picture') { - allowList.push('picture'); - } + if (field === 'picture') allowList.push('picture'); } - return memo; }, {}); payload.uid = uid; - await user.updateProfile(uid, payload, allowList); }; - OAuth.getUidByOAuthid = async (name, oAuthid) => db.getObjectField(`${name}Id:uid`, oAuthid); - OAuth.deleteUserData = async (data) => { const names = await db.getSortedSetMembers('oauth2-multiple:strategies'); const oAuthIds = await user.getUserFields(data.uid, names.map(name => `${name}Id`)); Object.keys(oAuthIds).forEach((prop) => { - if (!names.includes(prop.replace(/Id$/, ''))) { - delete oAuthIds[prop]; - } + if (!names.includes(prop.replace(/Id$/, ''))) delete oAuthIds[prop]; }); - const promises = []; - for (const [provider, id] of Object.entries(oAuthIds)) { - if (id) { - promises.push(db.deleteObjectField(`${provider}:uid`, id)); - } + for (const [providerField, id] of Object.entries(oAuthIds)) { + if (id) promises.push(db.deleteObjectField(`${providerField}:uid`, id)); } - await Promise.all(promises); }; - -// If this filter is not there, the deleteUserData function will fail when getting the oauthId for deletion. OAuth.whitelistFields = async (params) => { const names = await db.getSortedSetMembers('oauth2-multiple:strategies'); params.whitelist.push(...names.map(name => `${name}Id`)); - return params; }; diff --git a/package-lock.json b/package-lock.json index 8f0e94f..91ad010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "async": "^3.2.0", + "lint": "^1.1.2", "node-fetch": "^2", "passport-oauth": "~1.0.0" }, @@ -249,31 +250,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@stylistic/eslint-plugin": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.2.tgz", @@ -361,9 +337,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "peer": true, @@ -412,6 +388,17 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -420,6 +407,20 @@ "node": ">=6.0.0" } }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -833,9 +834,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC", "peer": true @@ -1016,6 +1017,17 @@ "node": ">= 0.8.0" } }, + "node_modules/lint": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lint/-/lint-1.1.2.tgz", + "integrity": "sha512-kOzKdN0iSdkx57102MxZd64yn32rHaB6xqBMN3hyCRiBV2xiurM75XWvTHWNK/SJBCddHPhjDLV+kNr3Uf1MGg==", + "bin": { + "node-lint": "bin/node-lint" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -1109,17 +1121,17 @@ } }, "node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "peer": true, "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1313,9 +1325,9 @@ } }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1626,9 +1638,9 @@ } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index e2f1bb2..d936f46 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "readmeFilename": "README.md", "dependencies": { "async": "^3.2.0", + "lint": "^1.1.2", "node-fetch": "^2", "passport-oauth": "~1.0.0" }, diff --git a/yarn.lock b/yarn.lock index 4f61f06..e7105f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,139 @@ dependencies: "@commitlint/config-angular-type-enum" "^17.4.0" +"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": + version "4.9.1" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/config-array@^0.23.0": + version "0.23.1" + resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz" + integrity sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA== + dependencies: + "@eslint/object-schema" "^3.0.1" + debug "^4.3.1" + minimatch "^10.1.1" + +"@eslint/config-helpers@^0.5.2": + version "0.5.2" + resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz" + integrity sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ== + dependencies: + "@eslint/core" "^1.1.0" + +"@eslint/core@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz" + integrity sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/js@^10.0.0": + version "10.0.1" + resolved "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz" + integrity sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA== + +"@eslint/object-schema@^3.0.1": + version "3.0.1" + resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz" + integrity sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg== + +"@eslint/plugin-kit@^0.6.0": + version "0.6.0" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz" + integrity sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ== + dependencies: + "@eslint/core" "^1.1.0" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@stylistic/eslint-plugin@^5.x": + version "5.2.2" + resolved "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.2.tgz" + integrity sha512-bE2DUjruqXlHYP3Q2Gpqiuj2bHq7/88FnuaS0FjeGGLCy+X6a07bGVuwtiOYnPSLHR6jmx5Bwdv+j7l8H+G97A== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/types" "^8.37.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + estraverse "^5.3.0" + picomatch "^4.0.3" + +"@types/esrecurse@^4.3.1": + version "4.3.1" + resolved "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz" + integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw== + +"@types/estree@^1.0.6", "@types/estree@^1.0.8": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@typescript-eslint/types@^8.37.0": + version "8.38.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz" + integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +ajv@^6.12.4: + version "6.14.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ansi-escapes@^7.0.0: version "7.1.1" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz" @@ -36,11 +169,23 @@ async@^3.2.0: resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + base64url@3.x.x: version "3.0.1" resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== +brace-expansion@^5.0.5: + version "5.0.5" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz" + integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== + dependencies: + balanced-match "^4.0.2" + cli-cursor@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz" @@ -66,6 +211,27 @@ commander@^14.0.3: resolved "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz" integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw== +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.3.1, debug@^4.3.2: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + emoji-regex@^10.3.0: version "10.6.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz" @@ -76,6 +242,11 @@ environment@^1.0.0: resolved "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz" integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-config-nodebb@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/eslint-config-nodebb/-/eslint-config-nodebb-2.0.1.tgz" @@ -83,16 +254,169 @@ eslint-config-nodebb@2.0.1: dependencies: globals "17.3.0" +eslint-scope@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz" + integrity sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ== + dependencies: + "@types/esrecurse" "^4.3.1" + "@types/estree" "^1.0.8" + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint-visitor-keys@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz" + integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q== + +eslint@^10.0.0, "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@>=9.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz" + integrity sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.2" + "@eslint/config-array" "^0.23.0" + "@eslint/config-helpers" "^0.5.2" + "@eslint/core" "^1.1.0" + "@eslint/plugin-kit" "^0.6.0" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.12.4" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^9.1.0" + eslint-visitor-keys "^5.0.0" + espree "^11.1.0" + esquery "^1.7.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + minimatch "^10.1.1" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + +espree@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz" + integrity sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^5.0.0" + +esquery@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.4.2" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== + get-east-asian-width@^1.0.0, get-east-asian-width@^1.3.0, get-east-asian-width@^1.3.1: version "1.4.0" resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz" integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q== +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + globals@17.3.0: version "17.3.0" resolved "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz" @@ -103,6 +427,21 @@ husky@9.1.7: resolved "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + is-fullwidth-code-point@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz" @@ -110,6 +449,48 @@ is-fullwidth-code-point@^5.0.0: dependencies: get-east-asian-width "^1.3.1" +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lint-staged@16.4.0: version "16.4.0" resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz" @@ -122,6 +503,11 @@ lint-staged@16.4.0: tinyexec "^1.0.4" yaml "^2.8.2" +lint@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/lint/-/lint-1.1.2.tgz" + integrity sha512-kOzKdN0iSdkx57102MxZd64yn32rHaB6xqBMN3hyCRiBV2xiurM75XWvTHWNK/SJBCddHPhjDLV+kNr3Uf1MGg== + listr2@^9.0.5: version "9.0.5" resolved "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz" @@ -134,6 +520,13 @@ listr2@^9.0.5: rfdc "^1.4.1" wrap-ansi "^9.0.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + log-update@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz" @@ -150,6 +543,23 @@ mimic-function@^5.0.0: resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz" integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== +minimatch@^10.1.1: + version "10.2.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz" + integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== + dependencies: + brace-expansion "^5.0.5" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + node-fetch@^2: version "2.6.12" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz" @@ -169,6 +579,40 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +passport-oauth@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/passport-oauth/-/passport-oauth-1.0.0.tgz" + integrity sha512-4IZNVsZbN1dkBzmEbBqUxDG8oFOIK81jqdksE3HEb/vI3ib3FMjbiZZ6MTtooyYZzmKu0BfovjvT1pdGgIq+4Q== + dependencies: + passport-oauth1 "1.x.x" + passport-oauth2 "1.x.x" + passport-oauth1@1.x.x: version "1.3.0" resolved "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.3.0.tgz" @@ -189,23 +633,35 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-oauth@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/passport-oauth/-/passport-oauth-1.0.0.tgz" - integrity sha512-4IZNVsZbN1dkBzmEbBqUxDG8oFOIK81jqdksE3HEb/vI3ib3FMjbiZZ6MTtooyYZzmKu0BfovjvT1pdGgIq+4Q== - dependencies: - passport-oauth1 "1.x.x" - passport-oauth2 "1.x.x" - passport-strategy@1.x.x: version "1.0.0" resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + picomatch@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" - integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + version "4.0.4" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz" + integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== restore-cursor@^5.0.0: version "5.1.0" @@ -220,6 +676,18 @@ rfdc@^1.4.1: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" @@ -272,11 +740,25 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + uid2@0.0.x: version "0.0.4" resolved "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz" integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + utils-merge@1.x.x: version "1.0.1" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" @@ -295,6 +777,18 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wrap-ansi@^9.0.0: version "9.0.2" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz" @@ -305,6 +799,11 @@ wrap-ansi@^9.0.0: strip-ansi "^7.1.0" yaml@^2.8.2: - version "2.8.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz" - integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + version "2.8.3" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz" + integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==