Session management in PDS client ()

* Move codegen api client into its own directory

* Implement session-aware client for pds

* Test pds session client, fixes

* Use pds lexicon rather than api types where possible

* Tidy
This commit is contained in:
devin ivy 2022-11-02 09:55:51 -04:00 committed by GitHub
parent 2fb128d94f
commit a6c5737bb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 1836 additions and 1441 deletions

@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env']],
}

@ -0,0 +1,6 @@
const base = require('../../jest.config.base.js')
module.exports = {
...base,
displayName: 'API',
}

@ -3,15 +3,18 @@
"version": "0.0.1",
"main": "src/index.ts",
"scripts": {
"codegen": "lex gen-api ./src ../../lexicons/atproto.com/* ../../lexicons/bsky.app/*",
"codegen": "lex gen-api ./src/client ../../lexicons/atproto.com/* ../../lexicons/bsky.app/*",
"build": "esbuild src/index.ts --define:process.env.NODE_ENV=\\\"production\\\" --bundle --platform=node --sourcemap --outfile=dist/index.js",
"postbuild" : "tsc --build tsconfig.build.json"
"postbuild": "tsc --build tsconfig.build.json",
"test": "jest"
},
"license": "MIT",
"dependencies": {
"@atproto/xrpc": "*"
"@atproto/xrpc": "*",
"typed-emitter": "^2.1.0"
},
"devDependencies": {
"@atproto/lex-cli": "*"
"@atproto/lex-cli": "*",
"@atproto/pds": "*"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

194
packages/api/src/session.ts Normal file

@ -0,0 +1,194 @@
import {
CallOptions,
Client as XrpcClient,
ServiceClient as XrpcServiceClient,
QueryParams,
ResponseType,
XRPCError,
XRPCResponse,
} from '@atproto/xrpc'
import EventEmitter from 'events'
import TypedEmitter from 'typed-emitter'
import { Client, ServiceClient } from './client'
import * as CreateSession from './client/types/com/atproto/createSession'
import * as RefreshSession from './client/types/com/atproto/refreshSession'
import * as CreateAccount from './client/types/com/atproto/createAccount'
const CREATE_SESSION = 'com.atproto.createSession'
const REFRESH_SESSION = 'com.atproto.refreshSession'
const DELETE_SESSION = 'com.atproto.deleteSession'
const CREATE_ACCOUNT = 'com.atproto.createAccount'
export class SessionClient extends Client {
service(serviceUri: string | URL): SessionServiceClient {
const xrpcService = new SessionXrpcServiceClient(this.xrpc, serviceUri)
return new SessionServiceClient(this, xrpcService)
}
}
const defaultInst = new SessionClient()
export default defaultInst
export class SessionServiceClient extends ServiceClient {
xrpc: SessionXrpcServiceClient
sessionManager: SessionManager
constructor(baseClient: Client, xrpcService: SessionXrpcServiceClient) {
super(baseClient, xrpcService)
this.sessionManager = this.xrpc.sessionManager
}
}
export class SessionXrpcServiceClient extends XrpcServiceClient {
sessionManager = new SessionManager()
refreshing?: Promise<XRPCResponse>
constructor(baseClient: XrpcClient, serviceUri: string | URL) {
super(baseClient, serviceUri)
this.sessionManager.on('session', () => {
// Maintain access token headers when session changes
const accessHeaders = this.sessionManager.accessHeaders()
if (accessHeaders) {
this.setHeader('authorization', accessHeaders.authorization)
} else {
this.unsetHeader('authorization')
}
})
}
async call(
methodNsid: string,
params?: QueryParams,
data?: unknown,
opts?: CallOptions,
) {
const original = (overrideOpts?: CallOptions) =>
super.call(methodNsid, params, data, overrideOpts ?? opts)
// If someone is setting credentials manually, pass through as an escape hatch
if (opts?.headers?.authorization) {
return await original()
}
// Manage concurrent refreshes on session refresh
if (methodNsid === REFRESH_SESSION) {
return await this.refresh(opts)
}
// Complete any pending session refresh and then continue onto the original request with fresh credentials
await this.refreshing
// Setup session on session or account creation
if (methodNsid === CREATE_SESSION || methodNsid === CREATE_ACCOUNT) {
const result = await original()
const { accessJwt, refreshJwt } =
result.data as CreateSession.OutputSchema & CreateAccount.OutputSchema
this.sessionManager.set({ accessJwt, refreshJwt })
return result
}
// Clear session on session deletion
if (methodNsid === DELETE_SESSION) {
const result = await original({
...opts,
headers: {
...opts?.headers,
...this.sessionManager.refreshHeaders(),
},
})
this.sessionManager.unset()
return result
}
// For all other requests, if failed due to an expired token, refresh and retry with fresh credentials
try {
return await original()
} catch (err) {
if (
err instanceof XRPCError &&
err.status === ResponseType.InvalidRequest &&
err.error === 'ExpiredToken' &&
this.sessionManager.active()
) {
await this.refresh(opts)
return await original()
}
throw err
}
}
// Ensures a single refresh request at a time, deduping concurrent requests.
async refresh(opts?: CallOptions) {
this.refreshing ??= this._refresh(opts)
try {
return await this.refreshing
} finally {
this.refreshing = undefined
}
}
private async _refresh(opts?: CallOptions) {
try {
const result = await super.call(REFRESH_SESSION, undefined, undefined, {
...opts,
headers: {
...opts?.headers,
...this.sessionManager.refreshHeaders(),
},
})
const { accessJwt, refreshJwt } =
result.data as RefreshSession.OutputSchema
this.sessionManager.set({ accessJwt, refreshJwt })
return result
} catch (err) {
if (
err instanceof XRPCError &&
err.status === ResponseType.InvalidRequest &&
(err.error === 'ExpiredToken' || err.error === 'InvalidToken')
) {
this.sessionManager.unset()
}
throw err
}
}
}
export class SessionManager extends (EventEmitter as new () => TypedEmitter<SessionEvents>) {
session?: Session
get() {
return this.session
}
set(session: Session) {
this.session = session
this.emit('session', session)
}
unset() {
this.session = undefined
this.emit('session', undefined)
}
active() {
return !!this.session
}
accessHeaders() {
return (
this.session && {
authorization: `Bearer ${this.session.accessJwt}`,
}
)
}
refreshHeaders() {
return (
this.session && {
authorization: `Bearer ${this.session.refreshJwt}`,
}
)
}
}
export type Session = {
refreshJwt: string
accessJwt: string
}
type SessionEvents = {
session: (session?: Session) => void
}

@ -0,0 +1,241 @@
import {
CloseFn,
runTestServer,
TestServerInfo,
} from '@atproto/pds/tests/_util'
import * as locals from '@atproto/pds/src/locals'
import { sessionClient, Session, SessionServiceClient } from '..'
describe('session', () => {
let server: TestServerInfo
let client: SessionServiceClient
let close: CloseFn
beforeAll(async () => {
server = await runTestServer({
dbPostgresSchema: 'session',
})
client = sessionClient.service(server.url)
close = server.close
})
afterAll(async () => {
await close()
})
it('manages a new session on account creation.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { data: account } = await client.com.atproto.createAccount(
{},
{ username: 'alice.test', email: 'alice@test.com', password: 'password' },
)
expect(client.sessionManager.active()).toEqual(true)
expect(sessions).toEqual([
{ accessJwt: account.accessJwt, refreshJwt: account.refreshJwt },
])
const { data: sessionInfo } = await client.com.atproto.getSession({})
expect(sessionInfo).toEqual({
did: account.did,
name: account.username,
})
})
it('ends a new session on session deletion.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
await client.com.atproto.deleteSession({})
expect(sessions).toEqual([undefined])
expect(client.sessionManager.active()).toEqual(false)
const getSessionAfterDeletion = client.com.atproto.getSession({})
await expect(getSessionAfterDeletion).rejects.toThrow(
'Authentication Required',
)
})
it('manages a new session on session creation.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { data: session } = await client.com.atproto.createSession(
{},
{ username: 'alice.test', password: 'password' },
)
expect(sessions).toEqual([
{ accessJwt: session.accessJwt, refreshJwt: session.refreshJwt },
])
expect(client.sessionManager.active()).toEqual(true)
const { data: sessionInfo } = await client.com.atproto.getSession({})
expect(sessionInfo).toEqual({
did: session.did,
name: session.name,
})
})
it('refreshes existing session.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { data: session } = await client.com.atproto.createSession(
{},
{ username: 'alice.test', password: 'password' },
)
const { data: sessionRefresh } = await client.com.atproto.refreshSession({})
expect(sessions).toEqual([
{ accessJwt: session.accessJwt, refreshJwt: session.refreshJwt },
{
accessJwt: sessionRefresh.accessJwt,
refreshJwt: sessionRefresh.refreshJwt,
},
])
expect(client.sessionManager.active()).toEqual(true)
const { data: sessionInfo } = await client.com.atproto.getSession({})
expect(sessionInfo).toEqual({
did: sessionRefresh.did,
name: sessionRefresh.name,
})
// Uses escape hatch: authorization set, so sessions are not managed by this call
const refreshStaleSession = client.com.atproto.refreshSession(
{},
undefined,
{
headers: { authorization: `Bearer ${session.refreshJwt}` },
},
)
await expect(refreshStaleSession).rejects.toThrow('Token has been revoked')
expect(sessions.length).toEqual(2)
expect(client.sessionManager.active()).toEqual(true)
})
it('dedupes concurrent refreshes.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { data: session } = await client.com.atproto.createSession(
{},
{ username: 'alice.test', password: 'password' },
)
const [{ data: sessionRefresh }] = await Promise.all(
[...Array(10)].map(() => client.com.atproto.refreshSession({})),
)
expect(sessions).toEqual([
{ accessJwt: session.accessJwt, refreshJwt: session.refreshJwt },
{
accessJwt: sessionRefresh.accessJwt,
refreshJwt: sessionRefresh.refreshJwt,
},
])
expect(client.sessionManager.active()).toEqual(true)
const { data: sessionInfo } = await client.com.atproto.getSession({})
expect(sessionInfo).toEqual({
did: sessionRefresh.did,
name: sessionRefresh.name,
})
})
it('manually sets and unsets existing session.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { data: session } = await client.com.atproto.createSession(
{},
{ username: 'alice.test', password: 'password' },
)
const sessionCreds = {
accessJwt: session.accessJwt,
refreshJwt: session.refreshJwt,
}
expect(client.sessionManager.active()).toEqual(true)
client.sessionManager.unset()
expect(client.sessionManager.active()).toEqual(false)
const getSessionAfterUnset = client.com.atproto.getSession({})
await expect(getSessionAfterUnset).rejects.toThrow(
'Authentication Required',
)
client.sessionManager.set(sessionCreds)
expect(client.sessionManager.active()).toEqual(true)
const { data: sessionInfo } = await client.com.atproto.getSession({})
expect(sessionInfo).toEqual({
did: session.did,
name: session.name,
})
expect(sessions).toEqual([sessionCreds, undefined, sessionCreds])
expect(client.sessionManager.active()).toEqual(true)
})
it('refreshes and retries request when access token is expired.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { auth } = locals.get(server.app)
const { data: sessionInfo } = await client.com.atproto.getSession({})
const accessExpired = await auth.createAccessToken(sessionInfo.did, -1)
expect(sessions.length).toEqual(0)
expect(client.sessionManager.active()).toEqual(true)
client.sessionManager.set({
refreshJwt: 'not-used-since-session-is-active',
...client.sessionManager.get(),
accessJwt: accessExpired.jwt,
})
expect(sessions.length).toEqual(1)
expect(client.sessionManager.active()).toEqual(true)
const { data: updatedSessionInfo } = await client.com.atproto.getSession({})
expect(updatedSessionInfo).toEqual(sessionInfo)
expect(sessions.length).toEqual(2) // New session was created during getSession()
expect(client.sessionManager.active()).toEqual(true)
})
it('unsets session when refresh token becomes expired.', async () => {
const sessions: (Session | undefined)[] = []
client.sessionManager.on('session', (session) => sessions.push(session))
const { auth } = locals.get(server.app)
const { data: sessionInfo } = await client.com.atproto.getSession({})
const accessExpired = await auth.createAccessToken(sessionInfo.did, -1)
const refreshExpired = await auth.createRefreshToken(sessionInfo.did, -1)
expect(sessions.length).toEqual(0)
expect(client.sessionManager.active()).toEqual(true)
client.sessionManager.set({
accessJwt: accessExpired.jwt,
refreshJwt: refreshExpired.jwt,
})
expect(sessions.length).toEqual(1)
expect(client.sessionManager.active()).toEqual(true)
const getSessionAfterExpired = client.com.atproto.getSession({})
await expect(getSessionAfterExpired).rejects.toThrow('Token has expired')
expect(sessions.length).toEqual(2)
expect(sessions[1]).toEqual(undefined)
expect(client.sessionManager.active()).toEqual(false)
})
})

@ -21,7 +21,6 @@
"migration:create": "ts-node ./bin/migration-create.ts"
},
"dependencies": {
"@atproto/api": "*",
"@atproto/auth": "*",
"@atproto/common": "*",
"@atproto/crypto": "*",
@ -49,6 +48,7 @@
"zod": "^3.14.2"
},
"devDependencies": {
"@atproto/api": "*",
"@atproto/lex-cli": "*",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",

@ -1,7 +1,7 @@
import { sql } from 'kysely'
import { QueryParams } from '@atproto/api/src/types/app/bsky/getUsersSearch'
import Database from '../../../db'
import { Server } from '../../../lexicon'
import * as Method from '../../../lexicon/types/app/bsky/getUsersSearch'
import * as locals from '../../../locals'
import {
cleanTerm,
@ -84,7 +84,7 @@ const getResultsSqlite: GetResultsFn = async (db, { term, limit, before }) => {
type GetResultsFn = (
db: Database,
opts: QueryParams & { limit: number },
opts: Method.QueryParams & { limit: number },
) => Promise<
{
did: string

@ -1,6 +1,6 @@
import { QueryParams } from '@atproto/api/src/types/app/bsky/getUsersTypeahead'
import Database from '../../../db'
import { Server } from '../../../lexicon'
import * as Method from '../../../lexicon/types/app/bsky/getUsersTypeahead'
import * as locals from '../../../locals'
import {
cleanTerm,
@ -70,5 +70,5 @@ const getResultsSqlite: GetResultsFn = async (db, { term, limit }) => {
type GetResultsFn = (
db: Database,
opts: QueryParams & { limit: number },
opts: Method.QueryParams & { limit: number },
) => Promise<{ did: string; name: string; displayName: string | null }[]>

@ -2,11 +2,8 @@ import { once, EventEmitter } from 'events'
import AtpApi, {
ServiceClient as AtpServiceClient,
ComAtprotoCreateAccount,
ComAtprotoResetAccountPassword as ResetAccountPassword,
} from '@atproto/api'
import {
ExpiredTokenError,
InvalidTokenError,
} from '@atproto/api/src/types/com/atproto/resetAccountPassword'
import * as plc from '@atproto/plc'
import * as crypto from '@atproto/crypto'
import { sign } from 'jsonwebtoken'
@ -407,7 +404,7 @@ describe('account', () => {
// Reuse of token fails
await expect(
client.com.atproto.resetAccountPassword({}, { token, password }),
).rejects.toThrow(InvalidTokenError)
).rejects.toThrow(ResetAccountPassword.InvalidTokenError)
// Logs in with new password and not previous password
await expect(
@ -445,7 +442,7 @@ describe('account', () => {
{},
{ token: expiredToken, password: passwordAlt },
),
).rejects.toThrow(ExpiredTokenError)
).rejects.toThrow(ResetAccountPassword.ExpiredTokenError)
// Still logs in with previous password
await expect(

@ -1,5 +1,5 @@
import AtpApi, { ServiceClient as AtpServiceClient } from '@atproto/api'
import * as Post from '@atproto/api/src/types/app/bsky/post'
import * as Post from '../src/lexicon/types/app/bsky/post'
import { AtUri } from '@atproto/uri'
import { CloseFn, paginateAll, runTestServer } from './_util'

@ -1,4 +1,5 @@
import AtpApi, { ServiceClient as AtpServiceClient } from '@atproto/api'
import * as HomeFeed from '../../src/lexicon/types/app/bsky/getHomeFeed'
import {
runTestServer,
forSnapshot,
@ -9,7 +10,6 @@ import {
import { SeedClient } from '../seeds/client'
import basicSeed from '../seeds/basic'
import { FeedAlgorithm } from '../../src/api/app/bsky/util/feed'
import { FeedItem } from '@atproto/api/src/types/app/bsky/getHomeFeed'
describe('pds home feed views', () => {
let client: AtpServiceClient
@ -41,7 +41,7 @@ describe('pds home feed views', () => {
})
it("fetches authenticated user's home feed w/ reverse-chronological algorithm", async () => {
const expectOriginatorFollowedBy = (did) => (item: FeedItem) => {
const expectOriginatorFollowedBy = (did) => (item: HomeFeed.FeedItem) => {
const originator = getOriginator(item)
if (did === originator) {
// The user sees their own posts, but the user does not expect to see their reposts
@ -97,7 +97,7 @@ describe('pds home feed views', () => {
})
it("fetches authenticated user's home feed w/ firehose algorithm", async () => {
const expectNotOwnRepostsBy = (did) => (item: FeedItem) => {
const expectNotOwnRepostsBy = (did) => (item: HomeFeed.FeedItem) => {
const originator = getOriginator(item)
if (did === originator) {
// The user sees their own posts, but the user does not expect to see their reposts

@ -35,7 +35,7 @@ export class Client {
serviceUri: string | URL,
methodNsid: string,
params?: QueryParams,
data?: any,
data?: unknown,
opts?: CallOptions,
) {
return this.service(serviceUri).call(methodNsid, params, data, opts)
@ -85,10 +85,14 @@ export class ServiceClient {
this.headers[key] = value
}
unsetHeader(key: string): void {
delete this.headers[key]
}
async call(
methodNsid: string,
params?: QueryParams,
data?: any,
data?: unknown,
opts?: CallOptions,
) {
const schema = this.baseClient.schemas.get(methodNsid)
@ -129,7 +133,7 @@ async function defaultFetchHandler(
httpUri: string,
httpMethod: string,
httpHeaders: Headers,
httpReqBody: any,
httpReqBody: unknown,
): Promise<FetchHandlerResponse> {
try {
const res = await fetch(httpUri, {

101
yarn.lock

@ -1598,11 +1598,6 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
@ -3117,14 +3112,6 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc"
integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==
"@types/prompt@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/prompt/-/prompt-1.1.3.tgz#357268cb36fcf91ee3d672d6a198c183ca962bca"
integrity sha512-y1fMzb7a/mQs4tynU8agMmV8ptbT0dkgNafrdsoEyLtbCggK5COJ/r5DGcaAHB5kdXTGh+8trHlENlsZkm+tOQ==
dependencies:
"@types/node" "*"
"@types/revalidator" "*"
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@ -3135,11 +3122,6 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/revalidator@*":
version "0.3.8"
resolved "https://registry.yarnpkg.com/@types/revalidator/-/revalidator-0.3.8.tgz#86e0b03b49736000ad42ce6b002725e74c6805ff"
integrity sha512-q6KSi3PklLGQ0CesZ/XuLwly4DXXlnJuucYOG9lrBqrP8rKiuPZThav2h2+pFjaheNpnT0qKK3i304QWIePeJw==
"@types/serve-static@*":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
@ -3523,16 +3505,6 @@ async-mutex@^0.4.0:
dependencies:
tslib "^2.4.0"
async@3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
async@^3.2.3:
version "3.2.4"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -3994,7 +3966,7 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^5.0.0, chalk@^5.0.1:
chalk@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.1.tgz#ca57d71e82bb534a296df63bbacc4a1c22b2a4b6"
integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==
@ -4172,11 +4144,6 @@ colorette@^2.0.7:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
colors@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==
columnify@^1.5.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3"
@ -4415,11 +4382,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
cycle@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
integrity sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@ -5291,11 +5253,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
eyes@0.1.x:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
fast-copy@^2.1.1:
version "2.1.7"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-2.1.7.tgz#affc9475cb4b555fb488572b2a44231d0c9fa39e"
@ -6362,7 +6319,7 @@ isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
isstream@0.1.x, isstream@~0.1.2:
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
@ -8498,17 +8455,6 @@ promise-retry@^2.0.1:
err-code "^2.0.2"
retry "^0.12.0"
prompt@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/prompt/-/prompt-1.3.0.tgz#b1f6d47cb1b6beed4f0660b470f5d3ec157ad7ce"
integrity sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==
dependencies:
"@colors/colors" "1.5.0"
async "3.2.3"
read "1.0.x"
revalidator "0.1.x"
winston "2.x"
prompts@^2.0.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@ -8747,7 +8693,7 @@ read-pkg@^5.2.0:
parse-json "^5.0.0"
type-fest "^0.6.0"
read@1, read@1.0.x, read@~1.0.1:
read@1, read@~1.0.1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
integrity sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==
@ -8963,11 +8909,6 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
revalidator@0.1.x:
version "0.1.8"
resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
integrity sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -9008,6 +8949,13 @@ rxjs@^6.6.0, rxjs@^6.6.3:
dependencies:
tslib "^1.9.0"
rxjs@^7.5.2:
version "7.5.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
dependencies:
tslib "^2.1.0"
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
@ -9336,11 +9284,6 @@ ssri@^8.0.0, ssri@^8.0.1:
dependencies:
minipass "^3.1.1"
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
stack-utils@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
@ -9769,6 +9712,11 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tslib@^2.3.1, tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
@ -9853,6 +9801,13 @@ type@^2.7.2:
resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typed-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb"
integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==
optionalDependencies:
rxjs "^7.5.2"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@ -10138,18 +10093,6 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
winston@2.x:
version "2.4.6"
resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.6.tgz#da616f332928f70aac482f59b43d62228f29e478"
integrity sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==
dependencies:
async "^3.2.3"
colors "1.0.x"
cycle "1.0.x"
eyes "0.1.x"
isstream "0.1.x"
stack-trace "0.0.x"
word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"