diff --git a/packages/backend/package.json b/packages/backend/package.json index 34d4b206a0..f6912d0944 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -90,6 +90,7 @@ "date-fns": "2.30.0", "deep-email-validator": "0.1.21", "escape-regexp": "0.0.1", + "express-session": "^1.17.3", "fastify": "4.18.0", "feed": "4.2.2", "file-type": "18.5.0", @@ -117,6 +118,8 @@ "nodemailer": "6.9.3", "nsfwjs": "2.4.2", "oauth": "0.10.0", + "oauth2orize": "^1.11.1", + "oauth2orize-pkce": "^0.1.2", "oidc-provider": "^8.1.1", "os-utils": "0.0.14", "otpauth": "9.1.2", @@ -171,6 +174,7 @@ "@types/color-convert": "2.0.0", "@types/content-disposition": "0.5.5", "@types/escape-regexp": "0.0.1", + "@types/express-session": "^1.17.6", "@types/fluent-ffmpeg": "2.1.21", "@types/jest": "29.5.2", "@types/js-yaml": "4.0.5", @@ -183,6 +187,7 @@ "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.8", "@types/oauth": "0.9.1", + "@types/oauth2orize": "^1.8.11", "@types/pg": "8.10.2", "@types/pug": "2.0.6", "@types/punycode": "2.1.0", diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index d86eaa5915..e63bcf0252 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -1,17 +1,28 @@ import dns from 'node:dns/promises'; import { Inject, Injectable } from '@nestjs/common'; import Provider, { type Adapter, type Account, AdapterPayload } from 'oidc-provider'; -import fastifyMiddie from '@fastify/middie'; +import fastifyMiddie, { IncomingMessageExtended } from '@fastify/middie'; import { JSDOM } from 'jsdom'; import parseLinkHeader from 'parse-link-header'; import ipaddr from 'ipaddr.js'; +import oauth2orize from 'oauth2orize'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { kinds } from '@/misc/api-permissions.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { FastifyInstance } from 'fastify'; +import fastifyCookie from '@fastify/cookie'; +import fastifySession from '@fastify/session'; import type Redis from 'ioredis'; +import oauth2Pkce from 'oauth2orize-pkce'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import expressSession from 'express-session'; +import http from 'node:http'; +import fastifyView from '@fastify/view'; +import pug from 'pug'; +import { fileURLToPath } from 'node:url'; +import { MetaService } from '@/core/MetaService.js'; // https://indieauth.spec.indieweb.org/#client-identifier function validateClientId(raw: string): URL { @@ -63,32 +74,32 @@ function validateClientId(raw: string): URL { return url; } -const grantable = new Set([ - 'AccessToken', - 'AuthorizationCode', - 'RefreshToken', - 'DeviceCode', - 'BackchannelAuthenticationRequest', -]); +// const grantable = new Set([ +// 'AccessToken', +// 'AuthorizationCode', +// 'RefreshToken', +// 'DeviceCode', +// 'BackchannelAuthenticationRequest', +// ]); -const consumable = new Set([ - 'AuthorizationCode', - 'RefreshToken', - 'DeviceCode', - 'BackchannelAuthenticationRequest', -]); +// const consumable = new Set([ +// 'AuthorizationCode', +// 'RefreshToken', +// 'DeviceCode', +// 'BackchannelAuthenticationRequest', +// ]); -function grantKeyFor(id: string): string { - return `grant:${id}`; -} +// function grantKeyFor(id: string): string { +// return `grant:${id}`; +// } -function userCodeKeyFor(userCode: string): string { - return `userCode:${userCode}`; -} +// function userCodeKeyFor(userCode: string): string { +// return `userCode:${userCode}`; +// } -function uidKeyFor(uid: string): string { - return `uid:${uid}`; -} +// function uidKeyFor(uid: string): string { +// return `uid:${uid}`; +// } async function fetchFromClientId(httpRequestService: HttpRequestService, id: string): Promise { try { @@ -107,179 +118,201 @@ async function fetchFromClientId(httpRequestService: HttpRequestService, id: str } } -class MisskeyAdapter implements Adapter { - name = 'oauth2'; +// class MisskeyAdapter implements Adapter { +// name = 'oauth2'; - constructor(private redisClient: Redis.Redis, private httpRequestService: HttpRequestService) { } +// constructor(private redisClient: Redis.Redis, private httpRequestService: HttpRequestService) { } - key(id: string): string { - return `oauth2:${id}`; - } +// key(id: string): string { +// return `oauth2:${id}`; +// } - async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise { - console.log('oauth upsert', id, payload, expiresIn); +// async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise { +// console.log('oauth upsert', id, payload, expiresIn); - const key = this.key(id); +// const key = this.key(id); - const multi = this.redisClient.multi(); - if (consumable.has(this.name)) { - multi.hset(key, { payload: JSON.stringify(payload) }); - } else { - multi.set(key, JSON.stringify(payload)); - } +// const multi = this.redisClient.multi(); +// if (consumable.has(this.name)) { +// multi.hset(key, { payload: JSON.stringify(payload) }); +// } else { +// multi.set(key, JSON.stringify(payload)); +// } - if (expiresIn) { - multi.expire(key, expiresIn); - } +// if (expiresIn) { +// multi.expire(key, expiresIn); +// } - if (grantable.has(this.name) && payload.grantId) { - const grantKey = grantKeyFor(payload.grantId); - multi.rpush(grantKey, key); - // if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM - // here to trim the list to an appropriate length - const ttl = await this.redisClient.ttl(grantKey); - if (expiresIn > ttl) { - multi.expire(grantKey, expiresIn); - } - } +// if (grantable.has(this.name) && payload.grantId) { +// const grantKey = grantKeyFor(payload.grantId); +// multi.rpush(grantKey, key); +// // if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM +// // here to trim the list to an appropriate length +// const ttl = await this.redisClient.ttl(grantKey); +// if (expiresIn > ttl) { +// multi.expire(grantKey, expiresIn); +// } +// } - if (payload.userCode) { - const userCodeKey = userCodeKeyFor(payload.userCode); - multi.set(userCodeKey, id); - multi.expire(userCodeKey, expiresIn); - } +// if (payload.userCode) { +// const userCodeKey = userCodeKeyFor(payload.userCode); +// multi.set(userCodeKey, id); +// multi.expire(userCodeKey, expiresIn); +// } - if (payload.uid) { - const uidKey = uidKeyFor(payload.uid); - multi.set(uidKey, id); - multi.expire(uidKey, expiresIn); - } +// if (payload.uid) { +// const uidKey = uidKeyFor(payload.uid); +// multi.set(uidKey, id); +// multi.expire(uidKey, expiresIn); +// } - await multi.exec(); - } +// await multi.exec(); +// } - async find(id: string): Promise { - console.log('oauth find', id); +// async find(id: string): Promise { +// console.log('oauth find', id); - // XXX: really? - const fromRedis = await this.findRedis(id); - if (fromRedis) { - return fromRedis; - } +// // XXX: really? +// const fromRedis = await this.findRedis(id); +// if (fromRedis) { +// return fromRedis; +// } - // Find client information from the remote. - const url = validateClientId(id); +// // Find client information from the remote. +// const url = validateClientId(id); - if (process.env.NODE_ENV !== 'test') { - const lookup = await dns.lookup(url.hostname); - if (ipaddr.parse(lookup.address).range() === 'loopback') { - throw new Error('client_id unexpectedly resolves to loopback IP.'); - } - } +// if (process.env.NODE_ENV !== 'test') { +// const lookup = await dns.lookup(url.hostname); +// if (ipaddr.parse(lookup.address).range() === 'loopback') { +// throw new Error('client_id unexpectedly resolves to loopback IP.'); +// } +// } - const redirectUri = await fetchFromClientId(this.httpRequestService, id); - if (!redirectUri) { - // IndieAuth also implicitly allows any path under the same scheme+host, - // but oidc-provider requires explicit list of uris. - throw new Error('The URL of client_id must provide `redirect_uri` as HTTP Link header or HTML element.'); - } +// const redirectUri = await fetchFromClientId(this.httpRequestService, id); +// if (!redirectUri) { +// // IndieAuth also implicitly allows any path under the same scheme+host, +// // but oidc-provider requires explicit list of uris. +// throw new Error('The URL of client_id must provide `redirect_uri` as HTTP Link header or HTML element.'); +// } - return { - client_id: id, - token_endpoint_auth_method: 'none', - redirect_uris: [redirectUri], - }; - } +// return { +// client_id: id, +// token_endpoint_auth_method: 'none', +// redirect_uris: [redirectUri], +// }; +// } - async findRedis(id: string | null): Promise { - if (!id) { - return; - } +// async findRedis(id: string | null): Promise { +// if (!id) { +// return; +// } - const data = consumable.has(this.name) - ? await this.redisClient.hgetall(this.key(id)) - : await this.redisClient.get(this.key(id)); +// const data = consumable.has(this.name) +// ? await this.redisClient.hgetall(this.key(id)) +// : await this.redisClient.get(this.key(id)); - if (!data || (typeof data === 'object' && !Object.entries(data).length)) { - return undefined; - } +// if (!data || (typeof data === 'object' && !Object.entries(data).length)) { +// return undefined; +// } - if (typeof data === 'string') { - return JSON.parse(data); - } - const { payload, ...rest } = data as any; - return { - ...rest, - ...JSON.parse(payload), - }; - } +// if (typeof data === 'string') { +// return JSON.parse(data); +// } +// const { payload, ...rest } = data as any; +// return { +// ...rest, +// ...JSON.parse(payload), +// }; +// } - async findByUserCode(userCode: string): Promise { - console.log('oauth findByUserCode', userCode); - const id = await this.redisClient.get(userCodeKeyFor(userCode)); - return this.findRedis(id); - } +// async findByUserCode(userCode: string): Promise { +// console.log('oauth findByUserCode', userCode); +// const id = await this.redisClient.get(userCodeKeyFor(userCode)); +// return this.findRedis(id); +// } - async findByUid(uid: string): Promise { - console.log('oauth findByUid', uid); - const id = await this.redisClient.get(uidKeyFor(uid)); - return this.findRedis(id); - } +// async findByUid(uid: string): Promise { +// console.log('oauth findByUid', uid); +// const id = await this.redisClient.get(uidKeyFor(uid)); +// return this.findRedis(id); +// } - async consume(id: string): Promise { - console.log('oauth consume', id); - await this.redisClient.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000)); - } +// async consume(id: string): Promise { +// console.log('oauth consume', id); +// await this.redisClient.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000)); +// } - async destroy(id: string): Promise { - console.log('oauth destroy', id); - const key = this.key(id); - await this.redisClient.del(key); - } +// async destroy(id: string): Promise { +// console.log('oauth destroy', id); +// const key = this.key(id); +// await this.redisClient.del(key); +// } - async revokeByGrantId(grantId: string): Promise { - console.log('oauth revokeByGrandId', grantId); - const multi = this.redisClient.multi(); - const tokens = await this.redisClient.lrange(grantKeyFor(grantId), 0, -1); - tokens.forEach((token) => multi.del(token)); - multi.del(grantKeyFor(grantId)); - await multi.exec(); - } -} +// async revokeByGrantId(grantId: string): Promise { +// console.log('oauth revokeByGrandId', grantId); +// const multi = this.redisClient.multi(); +// const tokens = await this.redisClient.lrange(grantKeyFor(grantId), 0, -1); +// tokens.forEach((token) => multi.del(token)); +// multi.del(grantKeyFor(grantId)); +// await multi.exec(); +// } +// } + +// function promisify(callback: T) { +// return (...args: Parameters) => { + +// args[args.length - 1](); +// }; +// } + +type OmitFirstElement = T extends [unknown, ...(infer R)] + ? R + : []; @Injectable() export class OAuth2ProviderService { - #provider: Provider; + // #provider: Provider; + #server = oauth2orize.createServer(); constructor( @Inject(DI.config) private config: Config, - @Inject(DI.redis) redisClient: Redis.Redis, - httpRequestService: HttpRequestService, + @Inject(DI.redis) + private redisClient: Redis.Redis, + private httpRequestService: HttpRequestService, + private metaService: MetaService, ) { - this.#provider = new Provider(config.url, { - clientAuthMethods: ['none'], - pkce: { - // This is the default, but be explicit here as we announce it below - methods: ['S256'], - }, - routes: { - // defaults to '/auth' but '/authorize' is more consistent with many - // other services eg. Mastodon/Twitter/Facebook/GitLab/GitHub/etc. - authorization: '/authorize', - }, - scopes: kinds, - async findAccount(ctx, id): Promise { - console.log(id); - return undefined; - }, - adapter(): MisskeyAdapter { - return new MisskeyAdapter(redisClient, httpRequestService); - }, - async renderError(ctx, out, error): Promise { - console.log(error); - }, - }); + // this.#provider = new Provider(config.url, { + // clientAuthMethods: ['none'], + // pkce: { + // // This is the default, but be explicit here as we announce it below + // methods: ['S256'], + // }, + // routes: { + // // defaults to '/auth' but '/authorize' is more consistent with many + // // other services eg. Mastodon/Twitter/Facebook/GitLab/GitHub/etc. + // authorization: '/authorize', + // }, + // scopes: kinds, + // async findAccount(ctx, id): Promise { + // console.log(id); + // return undefined; + // }, + // adapter(): MisskeyAdapter { + // return new MisskeyAdapter(redisClient, httpRequestService); + // }, + // async renderError(ctx, out, error): Promise { + // console.log(error); + // }, + // }); + this.#server.grant(oauth2Pkce.extensions()); + this.#server.grant(oauth2orize.grant.code((client, redirectUri, user, ares, done) => { + console.log(client, redirectUri, user, ares); + const code = secureRndstr(32, true); + done(null, code); + })); + this.#server.serializeClient((client, done) => done(null, client)); + this.#server.deserializeClient((id, done) => done(null, id)); } // Return 404 for any unknown paths under /oauth so that clients can know @@ -316,12 +349,65 @@ export class OAuth2ProviderService { // no way to turn it off. // For now only allow the basic OAuth endpoints, to start small and evaluate // this feature for some time, given that this is security related. - fastify.get('/oauth/authorize', async () => { }); + fastify.get<{ Querystring: { code_challenge?: string, code_challenge_method?: string } }>('/oauth/authorize', async (request, reply) => { + console.log('HIT /oauth/authorize', request.query); + if (typeof request.query.code_challenge !== 'string') { + throw new Error('`code_challenge` parameter is required'); + } + if (request.query.code_challenge_method !== 'S256') { + throw new Error('`code_challenge_method` parameter must be set as S256'); + } + + const meta = await this.metaService.fetch(); + return await reply.view('base', { + img: meta.bannerUrl, + title: meta.name ?? 'Misskey', + instanceName: meta.name ?? 'Misskey', + url: this.config.url, + desc: meta.description, + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); + }); fastify.post('/oauth/token', async () => { }); - fastify.get('/oauth/interaction/:uid', async () => { }); - fastify.get('/oauth/interaction/:uid/login', async () => { }); + // fastify.get('/oauth/interaction/:uid', async () => { }); + // fastify.get('/oauth/interaction/:uid/login', async () => { }); + + fastify.register(fastifyView, { + root: fileURLToPath(new URL('../web/views', import.meta.url)), + engine: { pug }, + defaultContext: { + version: this.config.version, + config: this.config, + }, + }); await fastify.register(fastifyMiddie); - fastify.use('/oauth', this.#provider.callback()); + fastify.use(expressSession({ secret: 'keyboard cat', resave: false, saveUninitialized: false }) as any); + fastify.use('/oauth/authorize', this.#server.authorization((clientId, redirectUri, done) => { + (async (): Promise>> => { + console.log('HIT /oauth/authorize validation middleware'); + + // Find client information from the remote. + const clientUrl = validateClientId(clientId); + const redirectUrl = new URL(redirectUri); + + if (process.env.NODE_ENV !== 'test') { + const lookup = await dns.lookup(clientUrl.hostname); + if (ipaddr.parse(lookup.address).range() === 'loopback') { + throw new Error('client_id unexpectedly resolves to loopback IP.'); + } + } + + if (redirectUrl.protocol !== clientUrl.protocol || redirectUrl.host !== clientUrl.host) { + // TODO: allow more redirect_uri by Client Information Discovery + throw new Error('cross-origin redirect_uri is not supported yet.'); + } + + return [clientId, redirectUri]; + })().then(args => done(null, ...args), err => done(err)); + })); + + // fastify.use('/oauth', this.#provider.callback()); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af9c08e95e..e52a4254cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: escape-regexp: specifier: 0.0.1 version: 0.0.1 + express-session: + specifier: ^1.17.3 + version: 1.17.3 fastify: specifier: 4.18.0 version: 4.18.0 @@ -266,6 +269,12 @@ importers: oauth: specifier: 0.10.0 version: 0.10.0 + oauth2orize: + specifier: ^1.11.1 + version: 1.11.1 + oauth2orize-pkce: + specifier: ^0.1.2 + version: 0.1.2 oidc-provider: specifier: ^8.1.1 version: 8.1.1 @@ -505,6 +514,9 @@ importers: '@types/escape-regexp': specifier: 0.0.1 version: 0.0.1 + '@types/express-session': + specifier: ^1.17.6 + version: 1.17.6 '@types/fluent-ffmpeg': specifier: 2.1.21 version: 2.1.21 @@ -541,6 +553,9 @@ importers: '@types/oauth': specifier: 0.9.1 version: 0.9.1 + '@types/oauth2orize': + specifier: ^1.8.11 + version: 1.8.11 '@types/pg': specifier: 8.10.2 version: 8.10.2 @@ -7562,6 +7577,12 @@ packages: '@types/range-parser': 1.2.4 dev: true + /@types/express-session@1.17.6: + resolution: {integrity: sha512-L6sB04HVA4HEZo1hDL65JXdZdBJtzZnCiw/P7MnO4w6746tJCNtXlHtzEASyI9ccn9zyOw6IbqQuhVa03VpO4w==} + dependencies: + '@types/express': 4.17.17 + dev: true + /@types/express@4.17.17: resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} dependencies: @@ -7782,6 +7803,13 @@ packages: resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} dev: true + /@types/oauth2orize@1.8.11: + resolution: {integrity: sha512-eir5IGegpcnPuhnx1Asdxj3kDWWP/Qr1qkERMlDASwlEJM6pppVBxkW7ZvAX2H8eBHE+FP7lhg/iNlRrtNGewQ==} + dependencies: + '@types/express': 4.17.17 + '@types/node': 20.3.1 + dev: true + /@types/oauth@0.9.1: resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} dependencies: @@ -10434,12 +10462,10 @@ packages: /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: true /cookie@0.4.2: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} - dev: true /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} @@ -11847,6 +11873,22 @@ packages: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} dev: false + /express-session@1.17.3: + resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + dev: false + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -16276,6 +16318,21 @@ packages: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} dev: false + /oauth2orize-pkce@0.1.2: + resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==} + dev: false + + /oauth2orize@1.11.1: + resolution: {integrity: sha512-9dSx/Gwm0J2Rvj4RH9+h7iXVnRXZ6biwWRgb2dCeQhCosODS0nYdM9I/G7BUGsjbgn0pHjGcn1zcCRtzj2SlRA==} + engines: {node: '>= 0.4.0'} + dependencies: + debug: 2.6.9 + uid2: 0.0.4 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /oauth@0.10.0: resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} dev: false @@ -16427,7 +16484,6 @@ packages: /on-headers@1.0.2: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} - dev: true /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -17777,6 +17833,11 @@ packages: resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} dev: true + /random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /random-seed@0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} engines: {node: '>= 0.6.0'} @@ -20173,6 +20234,17 @@ packages: dev: true optional: true + /uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 + dev: false + + /uid2@0.0.4: + resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} + dev: false + /uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -20453,7 +20525,6 @@ packages: /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - dev: true /uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}