From 94266dfa8f9a7e09ae4b54072579f13b0ad4cb01 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 31 Jan 2026 15:14:56 +0300 Subject: [PATCH] feat: add passport module --- .gitea/workflows/publish.yml | 45 +++ .gitignore | 8 + .npmignore | 10 + package.json | 28 ++ prettier.config.mjs | 7 + src/constants/index.ts | 1 + src/constants/passport.constants.ts | 1 + src/index.ts | 3 + src/interfaces/index.ts | 3 + src/interfaces/passport-async-options.ts | 8 + src/interfaces/passport-options.interface.ts | 3 + src/interfaces/token.interface.ts | 15 + src/passport.module.ts | 40 +++ src/passport.provider.ts | 30 ++ src/passport.service.ts | 91 ++++++ src/utils/base64.ts | 21 ++ src/utils/crypto.ts | 18 ++ src/utils/index.ts | 2 + tsconfig.build.json | 9 + tsconfig.json | 15 + yarn.lock | 317 +++++++++++++++++++ 21 files changed, 675 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 package.json create mode 100644 prettier.config.mjs create mode 100644 src/constants/index.ts create mode 100644 src/constants/passport.constants.ts create mode 100644 src/index.ts create mode 100644 src/interfaces/index.ts create mode 100644 src/interfaces/passport-async-options.ts create mode 100644 src/interfaces/passport-options.interface.ts create mode 100644 src/interfaces/token.interface.ts create mode 100644 src/passport.module.ts create mode 100644 src/passport.provider.ts create mode 100644 src/passport.service.ts create mode 100644 src/utils/base64.ts create mode 100644 src/utils/crypto.ts create mode 100644 src/utils/index.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..8a099ce --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish + +on: + push: + branches: + - main + +jobs: + Publish npm package action: + runs-on: ubuntu-latest + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + + - name: 'Setup dependencies' + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/ + scope: '@teacinema' + + - name: 'Enable corepack' + run: corepack enable + + - name: Create .npmrc for install dependencies + run: | + touch .npmrc + echo "@teacinema:registry=https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/" >> .npmrc + echo "//git.ksv741.keenetic.pro/api/packages/teacinema/npm/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} + + - name: 'Install dependencies' + run: yarn install --frozen-lockfile + + - name: 'Build' + run: yarn run build + + - name: Remove .npmrc + run: rm .npmrc + + - name: 'Publish to Gitea npm registry' + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0501cec --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/node_modules/ +dist/ +build/ +pnpm-lock.yaml +.env +.DS_Store +.log +.vscode/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..89ec62d --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +/node_modules/ +*.log +pnpm-lock.yaml +yarn.lock +.env +.DS_Store +.log +.vscode/ +.gitea/ +Thumbs.db diff --git a/package.json b/package.json new file mode 100644 index 0000000..58e74f9 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@teacinema/passport", + "version": "1.0.0", + "description": "TeaCinema authentication library", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@teacinema/core": "^1.0.9", + "@types/node": "^25.1.0", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + }, + "dependencies": { + "@nestjs/common": "^11.1.12", + "@nestjs/core": "^11.1.12", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + } +} diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..089d8f3 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,7 @@ +import config from '@teacinema/core/prettier' + +export default { + ...config, + tabWidth: 2, + useTabs: false, +}; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..380998f --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export * from './passport.constants' diff --git a/src/constants/passport.constants.ts b/src/constants/passport.constants.ts new file mode 100644 index 0000000..c242b6d --- /dev/null +++ b/src/constants/passport.constants.ts @@ -0,0 +1 @@ + export const PASSPORT_OPTIONS = Symbol('PassportOptions'); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2472f1f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './interfaces' +export * from './passport.module' +export * from './passport.service' diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..8e61300 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './passport-options.interface' +export * from './passport-async-options' +export * from './token.interface' diff --git a/src/interfaces/passport-async-options.ts b/src/interfaces/passport-async-options.ts new file mode 100644 index 0000000..b656de7 --- /dev/null +++ b/src/interfaces/passport-async-options.ts @@ -0,0 +1,8 @@ +import { FactoryProvider, ModuleMetadata } from '@nestjs/common' + +import { PassportOptions } from './passport-options.interface' + +export interface PassportAsyncOptions extends Pick { + useFactory: (...args: any[]) => Promise | PassportOptions + inject?: FactoryProvider['inject'] +} diff --git a/src/interfaces/passport-options.interface.ts b/src/interfaces/passport-options.interface.ts new file mode 100644 index 0000000..02d3aaa --- /dev/null +++ b/src/interfaces/passport-options.interface.ts @@ -0,0 +1,3 @@ +export interface PassportOptions { + secretKey: string; +} diff --git a/src/interfaces/token.interface.ts b/src/interfaces/token.interface.ts new file mode 100644 index 0000000..6605508 --- /dev/null +++ b/src/interfaces/token.interface.ts @@ -0,0 +1,15 @@ +export interface TokenPayload { + sub: string | number +} + +interface FailedResult { + valid: false; + reason: string; +} + +interface SuccessResult { + valid: true; + userId: string; +} + +export type VerifyResult = SuccessResult | FailedResult; diff --git a/src/passport.module.ts b/src/passport.module.ts new file mode 100644 index 0000000..6cc438e --- /dev/null +++ b/src/passport.module.ts @@ -0,0 +1,40 @@ +import { DynamicModule, Global, Module } from '@nestjs/common'; + + + +import { PASSPORT_OPTIONS } from './constants'; +import { PassportAsyncOptions, PassportOptions } from './interfaces' +import { + createPassportAsyncOptionsProvider, + createPassportOptionsProvider +} from './passport.provider' +import { PassportService } from './passport.service'; + + + + + +@Global() +@Module({}) +export class PassportModule { + public static register(options: PassportOptions): DynamicModule { + const optionProvider = createPassportOptionsProvider(options) + + return { + module: PassportModule, + providers: [optionProvider, PassportService], + exports: [PassportService, PASSPORT_OPTIONS] + } + } + + public static registerAsync(options: PassportAsyncOptions): DynamicModule { + const optionProvider = createPassportAsyncOptionsProvider(options) + + return { + module: PassportModule, + imports: options.imports ?? [], + providers: [optionProvider, PassportService], + exports: [PassportService, PASSPORT_OPTIONS] + } + } +} diff --git a/src/passport.provider.ts b/src/passport.provider.ts new file mode 100644 index 0000000..4d89bf0 --- /dev/null +++ b/src/passport.provider.ts @@ -0,0 +1,30 @@ +import { Provider } from '@nestjs/common' + +import { PASSPORT_OPTIONS } from './constants' +import { PassportAsyncOptions, PassportOptions } from './interfaces' + +export function createPassportOptionsProvider( + options: PassportOptions +): Provider { + return { + provide: PASSPORT_OPTIONS, + useValue: Object.freeze({ ...options }) + } +} + +export function createPassportAsyncOptionsProvider( + options: PassportAsyncOptions +): Provider { + return { + provide: PASSPORT_OPTIONS, + useFactory: async (...args: any[]) => { + const resolved = await options.useFactory(...args); + if (!resolved || typeof resolved.secretKey !== 'string') { + throw new Error('[PassportModule] "secreyKey" must be a string'); + } + + return Object.freeze({ ...resolved }) + }, + inject: options.inject || [], + } +} diff --git a/src/passport.service.ts b/src/passport.service.ts new file mode 100644 index 0000000..453d306 --- /dev/null +++ b/src/passport.service.ts @@ -0,0 +1,91 @@ +import { Inject, Injectable } from '@nestjs/common' + +import { PASSPORT_OPTIONS } from './constants' +import { PassportOptions } from './interfaces' +import { + base64UrlDecode, + base64UrlEncode, + computeHmac, + constantTimeEqual +} from './utils' + +@Injectable() +export class PassportService { + private static readonly HMAC_DOMAIN = 'PassportTokenAuth/v1' + private static readonly INTERNAL_SEP = '|' + private static readonly EXTERNAL_SEP = '.' + + private readonly SECRET_KEY: string + + constructor( + @Inject(PASSPORT_OPTIONS) private readonly options: PassportOptions + ) { + this.SECRET_KEY = options.secretKey + } + + private now() { + return Math.floor(Date.now() / 1000) + } + + private serialize(user: string, iat: string, ext: string) { + return [PassportService.HMAC_DOMAIN, user, iat, ext].join( + PassportService.INTERNAL_SEP + ) + } + + public generate(userId: string, ttl: number) { + const issuedAt = this.now() + const expiresAt = issuedAt + ttl + + const userPart = base64UrlEncode(userId) + const iatPart = base64UrlEncode(issuedAt.toString()) + const extPart = base64UrlEncode(expiresAt.toString()) + + const serialized = this.serialize(userPart, iatPart, extPart) + const hmac = computeHmac(this.SECRET_KEY, serialized) + + return [userPart, iatPart, extPart, hmac].join(PassportService.EXTERNAL_SEP) + } + + public verify(token: string) { + const parts = token.split(PassportService.EXTERNAL_SEP) + + if (parts.length !== 4) { + return { + valid: false, + reason: 'Invalid token format' + } + } + + const [userPart, iatPart, extPart, hmac] = parts + const serialized = this.serialize(userPart, iatPart, extPart) + const computedHmac = computeHmac(this.SECRET_KEY, serialized) + + if (!constantTimeEqual(hmac, computedHmac)) { + return { + valid: false, + reason: 'Invalid signature' + } + } + + const expNumber = Number(base64UrlDecode(extPart)) + if (!Number.isFinite(expNumber)) { + return { + valid: false, + reason: 'Error' + } + } + + if (this.now() > expNumber) { + return { + valid: false, + reason: 'Token expired' + } + } + + return { + valid: true, + userId: base64UrlDecode(userPart) + } + } +} diff --git a/src/utils/base64.ts b/src/utils/base64.ts new file mode 100644 index 0000000..fc965a6 --- /dev/null +++ b/src/utils/base64.ts @@ -0,0 +1,21 @@ +function base64UrlEncode(buf: Buffer | string) { + const s = typeof buf === 'string' ? Buffer.from(buf) : buf + + return s + .toString('base64url') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') +} + +function base64UrlDecode(str: string) { + str = str.replace(/-/g, '+').replace(/_/g, '/') + + while (str.length % 4) { + str += '=' + } + + return Buffer.from(str, 'base64').toString() +} + +export { base64UrlEncode, base64UrlDecode } diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 0000000..d55ab7e --- /dev/null +++ b/src/utils/crypto.ts @@ -0,0 +1,18 @@ +import { createHmac, timingSafeEqual } from 'node:crypto' + +function constantTimeEqual(a: string, b: string) { + const bufA = Buffer.from(a) + const bufB = Buffer.from(b) + + if (bufA.length !== bufB.length) { + return false + } + + return timingSafeEqual(bufA, bufB) +} + +function computeHmac(secretKey: string, data: string) { + return createHmac('sha256', secretKey).update(data).digest('hex') +} + +export { constantTimeEqual, computeHmac } diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..889470c --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './base64' +export * from './crypto' diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..9fff834 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "dist", + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..92c82fd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2024", + "types": ["node"], + "strict": true, + "strictNullChecks": false, + "jsx": "react-jsx", + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..1b6712e --- /dev/null +++ b/yarn.lock @@ -0,0 +1,317 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" + integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/generator@^7.26.5", "@babel/generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== + dependencies: + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/parser@^7.26.7", "@babel/parser@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.26.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" + integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/generator" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.6" + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + debug "^4.3.1" + +"@babel/types@^7.26.7", "@babel/types@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@borewit/text-codec@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@borewit/text-codec/-/text-codec-0.2.1.tgz#5d171538907a8cb395fdc2eb5e8f7947d96c7f2f" + integrity sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw== + +"@jridgewell/gen-mapping@^0.3.12": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@lukeed/csprng@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" + integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== + +"@nestjs/common@^11.1.12": + version "11.1.12" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.12.tgz#bd28d34fa6bf676671c70be8148b5405ccd3d422" + integrity sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw== + dependencies: + uid "2.0.2" + file-type "21.3.0" + iterare "1.2.1" + load-esm "1.0.3" + tslib "2.8.1" + +"@nestjs/core@^11.1.12": + version "11.1.12" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.12.tgz#cdbff023cc87ed071aebafeacb209bc5997da1e4" + integrity sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA== + dependencies: + uid "2.0.2" + "@nuxt/opencollective" "0.4.1" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + path-to-regexp "8.3.0" + tslib "2.8.1" + +"@nuxt/opencollective@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@nuxt/opencollective/-/opencollective-0.4.1.tgz#57bc41d2b03b2fba20b935c15950ac0f4bd2cea2" + integrity sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ== + dependencies: + consola "^3.2.3" + +"@teacinema/core@^1.0.9": + version "1.0.9" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcore/-/1.0.9/core-1.0.9.tgz#deec0675c6cc03fe2511a6b8dc5d6b6763998708" + integrity sha512-HM6JMOsvWL8xC2RxabIk4zdNjfRsBf9Gc9UEGk7O7X/ZLEvnt7hOVE7p3gsTuMQXeUK4qNjltm2vOU5U8M2ufA== + dependencies: + "@trivago/prettier-plugin-sort-imports" "^5.2.2" + +"@tokenizer/inflate@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.4.1.tgz#fa6cdb8366151b3cc8426bf9755c1ea03a2fba08" + integrity sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA== + dependencies: + debug "^4.4.3" + token-types "^6.1.1" + +"@tokenizer/token@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" + integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== + +"@trivago/prettier-plugin-sort-imports@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz#38983f0b83490a0a7d974a6f1e409fb4bf678d02" + integrity sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA== + dependencies: + "@babel/generator" "^7.26.5" + "@babel/parser" "^7.26.7" + "@babel/traverse" "^7.26.7" + "@babel/types" "^7.26.7" + javascript-natural-sort "^0.7.1" + lodash "^4.17.21" + +"@types/node@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.1.0.tgz#95cc584f1f478301efc86de4f1867e5875e83571" + integrity sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA== + dependencies: + undici-types "~7.16.0" + +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== + +debug@^4.3.1, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +fast-safe-stringify@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +file-type@21.3.0: + version "21.3.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-21.3.0.tgz#03334acfe59cc22f72fce2a1327c5a1190f0b7de" + integrity sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA== + dependencies: + "@tokenizer/inflate" "^0.4.1" + strtok3 "^10.3.4" + token-types "^6.1.1" + uint8array-extras "^1.4.0" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +javascript-natural-sort@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" + integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +load-esm@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/load-esm/-/load-esm-1.0.3.tgz#2073afe3da63902c323e80d9f135c301173ac92c" + integrity sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA== + +lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +path-to-regexp@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" + integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +prettier@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== + +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + +rxjs@^7.8.2: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + +strtok3@^10.3.4: + version "10.3.4" + resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-10.3.4.tgz#793ebd0d59df276a085586134b73a406e60be9c1" + integrity sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg== + dependencies: + "@tokenizer/token" "^0.3.0" + +token-types@^6.1.1: + version "6.1.2" + resolved "https://registry.yarnpkg.com/token-types/-/token-types-6.1.2.tgz#18d0fd59b996d421f9f83914d6101c201bd08129" + integrity sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww== + dependencies: + "@borewit/text-codec" "^0.2.1" + "@tokenizer/token" "^0.3.0" + ieee754 "^1.2.1" + +tslib@2.8.1, tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +uid@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" + integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== + dependencies: + "@lukeed/csprng" "^1.0.0" + +uint8array-extras@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz#10d2a85213de3ada304fea1c454f635c73839e86" + integrity sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A== + +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==