diff --git a/packages/connector-kakao/README.md b/packages/connector-kakao/README.md new file mode 100644 index 000000000..a98c0b871 --- /dev/null +++ b/packages/connector-kakao/README.md @@ -0,0 +1,54 @@ +# Kakao Connector + +The Kakao connector provides a succinct way for your application to use Kakao’s OAuth 2.0 authentication system. + +**Table of contents** +- [Set up a project in the Kakao Devlopers Console](#set-up-a-project-in-the-kakao-devlopers-console) +- [Configure Kakao Login](#configure-kakao-login) + - [Activate Kakao Login](#activate-kakao-login) + - [Privacy Setting](#privacy-setting) + - [Security Setting (Optional)](#security-setting-optional) +- [Configure Logto](#configure-logto) + - [Config types](#config-types) + - [clientId](#clientid) + - [clientSecret](#clientseceret) + +## Set up a project in the Kakao Devlopers Console +- Visit the [Kakao Developers Console](https://developers.kakao.com/console/app) and sign in with your Kakao account. +- Click the **Add an application** to create new project or choose exist project. + +## Configure Kakao Login + +### Activate Kakao Login +- Click the **Product Settings -> Kakao Login** from the menu. +- Turn on `Kakao Login Activation` +- Add below URL into `Redirect URI` + - `http(s)://YOUR_URL/callback/kakao-universal` + - (Please replace `YOUR_URL` with your `Logto` URL, and choose `http` or `https` on your situation.) + +### Privacy Setting +- Click the **Product Settings -> Kakao Login -> Consent Item** from the menu. +- Change state of `Nickname`, `Profile image`, and `Email` to **Required consent** (You might not able to change `Email` to **Required consent** because of your project setting.) + + +### Security Setting (Optional) +- Click the **Product Settings -> Kakao Login -> Security** from the menu. +- Click the `Client secret code` to generate secret code. +- Change `Activation state` to Enable. (If you enable it, `secret code` is necessary.) + +## Configure Logto + +### Config types + +| Name | Type | +|--------------|---------| +| clientId | string | +| clientSecret | string? | + +#### clientId +`clientId` is `REST API key` of your project. +(You can find it from `summary` of your project from Kakao developers console.) + +#### clientSeceret +`clientSecret` is `Secret Code` of your project. +(Please check [Security Setting (Optional)](#security-setting-optional)) diff --git a/packages/connector-kakao/docs/config-template.json b/packages/connector-kakao/docs/config-template.json new file mode 100644 index 000000000..081079e46 --- /dev/null +++ b/packages/connector-kakao/docs/config-template.json @@ -0,0 +1,4 @@ +{ + "clientId": "", + "clientSecret": "" +} diff --git a/packages/connector-kakao/jest.config.ts b/packages/connector-kakao/jest.config.ts new file mode 100644 index 000000000..5f612bc8c --- /dev/null +++ b/packages/connector-kakao/jest.config.ts @@ -0,0 +1,7 @@ +import { Config, merge } from '@silverhand/jest-config'; + +const config: Config.InitialOptions = merge({ + setupFilesAfterEnv: ['jest-matcher-specific-error'], +}); + +export default config; diff --git a/packages/connector-kakao/logo.svg b/packages/connector-kakao/logo.svg new file mode 100644 index 000000000..fce8c2dc6 --- /dev/null +++ b/packages/connector-kakao/logo.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/connector-kakao/package.json b/packages/connector-kakao/package.json new file mode 100644 index 000000000..aeebd48f0 --- /dev/null +++ b/packages/connector-kakao/package.json @@ -0,0 +1,54 @@ +{ + "name": "@logto/connector-kakao", + "version": "1.0.0-beta.6", + "description": "Kakao connector implementation.", + "main": "./lib/index.js", + "exports": "./lib/index.js", + "author": "Kyungyoon Kim. ", + "license": "MPL-2.0", + "private": true, + "files": [ + "lib", + "docs", + "logo.svg", + "README.md" + ], + "scripts": { + "precommit": "lint-staged", + "build": "rm -rf lib/ && tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", + "lint": "eslint --ext .ts src", + "lint:report": "pnpm lint --format json --output-file report.json", + "test": "jest", + "test:coverage": "jest --coverage --silent", + "prepack": "pnpm build" + }, + "dependencies": { + "@logto/connector-core": "^1.0.0-beta.5", + "@silverhand/essentials": "^1.2.0", + "@silverhand/jest-config": "1.0.0-rc.3", + "got": "^11.8.2", + "zod": "^3.14.3" + }, + "devDependencies": { + "@jest/types": "^28.1.3", + "@silverhand/eslint-config": "1.0.0-rc.2", + "@silverhand/ts-config": "1.0.0-rc.2", + "@types/jest": "^28.1.6", + "@types/node": "^16.3.1", + "eslint": "^8.21.0", + "jest": "^28.1.3", + "jest-matcher-specific-error": "^1.0.0", + "lint-staged": "^13.0.0", + "nock": "^13.2.2", + "prettier": "^2.7.1", + "typescript": "^4.7.4" + }, + "engines": { + "node": "^16.0.0" + }, + "eslintConfig": { + "extends": "@silverhand" + }, + "prettier": "@silverhand/eslint-config/.prettierrc" +} diff --git a/packages/connector-kakao/src/constant.ts b/packages/connector-kakao/src/constant.ts new file mode 100644 index 000000000..fa9d20694 --- /dev/null +++ b/packages/connector-kakao/src/constant.ts @@ -0,0 +1,30 @@ +import { ConnectorMetadata, ConnectorPlatform, ConnectorType } from '@logto/connector-core'; + +export const authorizationEndpoint = 'https://kauth.kakao.com/oauth/authorize'; +export const accessTokenEndpoint = 'https://kauth.kakao.com/oauth/token'; +export const userInfoEndpoint = 'https://kapi.kakao.com/v2/user/me'; + +export const defaultMetadata: ConnectorMetadata = { + id: 'kakao-universal', + target: 'kakao', + type: ConnectorType.Social, + platform: ConnectorPlatform.Universal, + name: { + en: 'Kakao', + 'zh-CN': 'Kakao', + 'tr-TR': 'Kakao', + 'ko-KR': '카카오', + }, + logo: './logo.svg', + logoDark: null, + description: { + en: 'Kakao is the most famous social network servcie provider in South Korea', + 'zh-CN': 'Kakao is the most famous social network servcie provider in South Korea', + 'tr-TR': 'Kakao is the most famous social network servcie provider in South Korea', + 'ko-KR': '카카오는 한국에서 가장 유명한 SNS 서비스 제공자 입니다.', + }, + readme: './README.md', + configTemplate: './docs/config-template.json', +}; + +export const defaultTimeout = 5000; diff --git a/packages/connector-kakao/src/index.test.ts b/packages/connector-kakao/src/index.test.ts new file mode 100644 index 000000000..d5536a9bf --- /dev/null +++ b/packages/connector-kakao/src/index.test.ts @@ -0,0 +1,138 @@ +import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-core'; +import nock from 'nock'; + +import createConnector, { getAccessToken } from '.'; +import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './constant'; +import { mockedConfig } from './mock'; + +const getConfig = jest.fn().mockResolvedValue(mockedConfig); + +describe('kakao connector', () => { + describe('getAuthorizationUri', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should get a valid authorizationUri with redirectUri and state', async () => { + const connector = await createConnector({ getConfig }); + const authorizationUri = await connector.getAuthorizationUri({ + state: 'some_state', + redirectUri: 'http://localhost:3000/callback', + }); + expect(authorizationUri).toEqual( + `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=some_state` + ); + }); + }); + + describe('getAccessToken', () => { + afterEach(() => { + nock.cleanAll(); + jest.clearAllMocks(); + }); + + it('should get an accessToken by exchanging with code', async () => { + nock(accessTokenEndpoint).post('').reply(200, { + access_token: 'access_token', + scope: 'scope', + token_type: 'token_type', + }); + const { accessToken } = await getAccessToken(mockedConfig, { + code: 'code', + redirectUri: 'dummyRedirectUri', + }); + expect(accessToken).toEqual('access_token'); + }); + + it('throws SocialAuthCodeInvalid error if accessToken not found in response', async () => { + nock(accessTokenEndpoint) + .post('') + .reply(200, { access_token: '', scope: 'scope', token_type: 'token_type' }); + await expect( + getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' }) + ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + }); + }); + + describe('getUserInfo', () => { + beforeEach(() => { + nock(accessTokenEndpoint).post('').reply(200, { + access_token: 'access_token', + scope: 'scope', + token_type: 'token_type', + }); + }); + + afterEach(() => { + nock.cleanAll(); + jest.clearAllMocks(); + }); + + it('should get valid SocialUserInfo', async () => { + nock(userInfoEndpoint) + .post('') + .reply(200, { + id: 1_234_567_890, + kakao_account: { + is_email_valid: true, + email: 'ruddbs5302@gmail.com', + profile: { + nickname: 'pemassi', + profile_image_url: 'https://github.com/images/error/octocat_happy.gif', + is_default_image: false, + }, + }, + }); + const connector = await createConnector({ getConfig }); + const socialUserInfo = await connector.getUserInfo({ + code: 'code', + redirectUri: 'redirectUri', + }); + expect(socialUserInfo).toMatchObject({ + id: '1234567890', + avatar: 'https://github.com/images/error/octocat_happy.gif', + name: 'pemassi', + email: 'ruddbs5302@gmail.com', + }); + }); + + it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => { + nock(userInfoEndpoint).post('').reply(401); + const connector = await createConnector({ getConfig }); + await expect(connector.getUserInfo({ code: 'code', redirectUri: '' })).rejects.toMatchError( + new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) + ); + }); + + it('throws General error', async () => { + nock(userInfoEndpoint).post('').reply(200, { + sub: '1234567890', + name: 'monalisa octocat', + given_name: 'monalisa', + family_name: 'octocat', + picture: 'https://github.com/images/error/octocat_happy.gif', + email: 'octocat@google.com', + email_verified: true, + locale: 'en', + }); + const connector = await createConnector({ getConfig }); + await expect( + connector.getUserInfo({ + error: 'general_error', + error_description: 'General error encountered.', + }) + ).rejects.toMatchError( + new ConnectorError( + ConnectorErrorCodes.General, + '{"error":"general_error","error_description":"General error encountered."}' + ) + ); + }); + + it('throws unrecognized error', async () => { + nock(userInfoEndpoint).post('').reply(500); + const connector = await createConnector({ getConfig }); + await expect(connector.getUserInfo({ code: 'code', redirectUri: '' })).rejects.toThrow(); + }); + }); +}); diff --git a/packages/connector-kakao/src/index.ts b/packages/connector-kakao/src/index.ts new file mode 100644 index 000000000..15f7665f4 --- /dev/null +++ b/packages/connector-kakao/src/index.ts @@ -0,0 +1,156 @@ +/** + * The Implementation of OpenID Connect of Kakao. + * https://developers.kakao.com/docs/latest/en/kakaologin/rest-api + */ +import { + ConnectorError, + ConnectorErrorCodes, + CreateConnector, + GetAuthorizationUri, + GetConnectorConfig, + GetUserInfo, + SocialConnector, + validateConfig, +} from '@logto/connector-core'; +import { assert, conditional } from '@silverhand/essentials'; +import got, { HTTPError } from 'got'; + +import { + accessTokenEndpoint, + authorizationEndpoint, + defaultMetadata, + defaultTimeout, + userInfoEndpoint, +} from './constant'; +import { + accessTokenResponseGuard, + authResponseGuard, + KakaoConfig, + kakaoConfigGuard, + userInfoResponseGuard, +} from './types'; + +const getAuthorizationUri = + (getConfig: GetConnectorConfig): GetAuthorizationUri => + async ({ state, redirectUri }) => { + const config = await getConfig(defaultMetadata.id); + validateConfig(config, kakaoConfigGuard); + + const queryParameters = new URLSearchParams({ + client_id: config.clientId, + redirect_uri: redirectUri, + response_type: 'code', + state, + }); + + return `${authorizationEndpoint}?${queryParameters.toString()}`; + }; + +export const getAccessToken = async ( + config: KakaoConfig, + codeObject: { code: string; redirectUri: string } +) => { + const { code, redirectUri } = codeObject; + const { clientId, clientSecret } = config; + + // Note:Need to decodeURIComponent on code + // https://stackoverflow.com/questions/51058256/google-api-node-js-invalid-grant-malformed-auth-code + const httpResponse = await got.post(accessTokenEndpoint, { + form: { + code: decodeURIComponent(code), + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + grant_type: 'authorization_code', + }, + timeout: defaultTimeout, + }); + + const result = accessTokenResponseGuard.safeParse(JSON.parse(httpResponse.body)); + + if (!result.success) { + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message); + } + + const { access_token: accessToken } = result.data; + + assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + + return { accessToken }; +}; + +const getUserInfo = + (getConfig: GetConnectorConfig): GetUserInfo => + // eslint-disable-next-line complexity + async (data) => { + const { code, redirectUri } = await authorizationCallbackHandler(data); + const config = await getConfig(defaultMetadata.id); + validateConfig(config, kakaoConfigGuard); + const { accessToken } = await getAccessToken(config, { code, redirectUri }); + + try { + const httpResponse = await got.post(userInfoEndpoint, { + headers: { + authorization: `Bearer ${accessToken}`, + }, + timeout: defaultTimeout, + }); + + const result = userInfoResponseGuard.safeParse(JSON.parse(httpResponse.body)); + + if (!result.success) { + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message); + } + + const { id, kakao_account } = result.data; + const { is_email_valid, email, profile } = kakao_account ?? { + is_email_valid: null, + profile: null, + email: null, + }; + + return { + id: id.toString(), + avatar: conditional(profile && !profile.is_default_image && profile.profile_image_url), + email: conditional(is_email_valid && email), + name: conditional(profile?.nickname), + }; + } catch (error: unknown) { + return getUserInfoErrorHandler(error); + } + }; + +const authorizationCallbackHandler = async (parameterObject: unknown) => { + const result = authResponseGuard.safeParse(parameterObject); + + if (!result.success) { + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject)); + } + + return result.data; +}; + +const getUserInfoErrorHandler = (error: unknown) => { + if (error instanceof HTTPError) { + const { statusCode, body: rawBody } = error.response; + + if (statusCode === 401) { + throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid); + } + + throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody)); + } + + throw error; +}; + +const createKakaoConnector: CreateConnector = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + configGuard: kakaoConfigGuard, + getAuthorizationUri: getAuthorizationUri(getConfig), + getUserInfo: getUserInfo(getConfig), + }; +}; + +export default createKakaoConnector; diff --git a/packages/connector-kakao/src/mock.ts b/packages/connector-kakao/src/mock.ts new file mode 100644 index 000000000..a52d77ccc --- /dev/null +++ b/packages/connector-kakao/src/mock.ts @@ -0,0 +1,4 @@ +export const mockedConfig = { + clientId: '', + clientSecret: '', +}; diff --git a/packages/connector-kakao/src/types.ts b/packages/connector-kakao/src/types.ts new file mode 100644 index 000000000..19717bcb8 --- /dev/null +++ b/packages/connector-kakao/src/types.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +export const kakaoConfigGuard = z.object({ + clientId: z.string(), + clientSecret: z.string().optional(), +}); + +export type KakaoConfig = z.infer; + +export const accessTokenResponseGuard = z.object({ + access_token: z.string(), + scope: z.string().optional(), + token_type: z.string(), +}); + +export type AccessTokenResponse = z.infer; + +export const userInfoResponseGuard = z.object({ + id: z.number(), + kakao_account: z + .object({ + is_email_valid: z.boolean().optional(), + email: z.string().optional(), + profile: z + .object({ + nickname: z.string().optional(), + profile_image_url: z.string().optional(), + is_default_image: z.boolean().optional(), + }) + .optional(), + }) + .optional(), +}); + +export type UserInfoResponse = z.infer; + +export const authResponseGuard = z.object({ + code: z.string(), + redirectUri: z.string(), +}); diff --git a/packages/connector-kakao/tsconfig.base.json b/packages/connector-kakao/tsconfig.base.json new file mode 100644 index 000000000..d4bcf4847 --- /dev/null +++ b/packages/connector-kakao/tsconfig.base.json @@ -0,0 +1,12 @@ +{ + "extends": "@silverhand/ts-config/tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + } + } +} diff --git a/packages/connector-kakao/tsconfig.build.json b/packages/connector-kakao/tsconfig.build.json new file mode 100644 index 000000000..a34d12e20 --- /dev/null +++ b/packages/connector-kakao/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.base", + "include": [ + "src" + ], + "exclude": [ + "src/**/*.test.ts" + ] +} diff --git a/packages/connector-kakao/tsconfig.json b/packages/connector-kakao/tsconfig.json new file mode 100644 index 000000000..c1c219b79 --- /dev/null +++ b/packages/connector-kakao/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.base", + "compilerOptions": { + "types": [ + "node", + "jest", + "jest-matcher-specific-error" + ] + }, + "include": [ + "src", + "jest.config.ts" + ] +} diff --git a/packages/connector-kakao/tsconfig.test.json b/packages/connector-kakao/tsconfig.test.json new file mode 100644 index 000000000..aa0bf1ab7 --- /dev/null +++ b/packages/connector-kakao/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "isolatedModules": false, + "allowJs": true + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 4485bb42e..5c181a2fc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@logto/core", - "version": "1.0.0-beta.6", + "version": "1.0.0-beta.5", "description": "The open source identity solution.", "main": "build/index.js", "author": "Silverhand Inc. ", @@ -34,6 +34,7 @@ "@logto/connector-twilio-sms": "^1.0.0-beta.6", "@logto/connector-wechat-native": "^1.0.0-beta.6", "@logto/connector-wechat-web": "^1.0.0-beta.6", + "@logto/connector-kakao": "^1.0.0-beta.6", "@logto/phrases": "^1.0.0-beta.6", "@logto/schemas": "^1.0.0-beta.6", "@logto/shared": "^1.0.0-beta.6", diff --git a/packages/core/src/connectors/consts.ts b/packages/core/src/connectors/consts.ts index 5adea3a4a..baf5fd113 100644 --- a/packages/core/src/connectors/consts.ts +++ b/packages/core/src/connectors/consts.ts @@ -17,6 +17,7 @@ export const defaultConnectorPackages = [ '@logto/connector-twilio-sms', '@logto/connector-wechat-web', '@logto/connector-wechat-native', + '@logto/connector-kakao', ]; const notImplemented = () => { diff --git a/packages/core/src/connectors/index.test.ts b/packages/core/src/connectors/index.test.ts index c700f6cca..5d38676b3 100644 --- a/packages/core/src/connectors/index.test.ts +++ b/packages/core/src/connectors/index.test.ts @@ -88,6 +88,12 @@ const wechatNativeConnector = { config: {}, createdAt: 1_646_382_233_000, }; +const kakaoConnector = { + id: 'kakao-universal', + enabled: false, + config: {}, + createdAt: 1_646_382_233_000, +}; const connectors = [ alipayConnector, @@ -104,6 +110,7 @@ const connectors = [ twilioSmsConnector, wechatConnector, wechatNativeConnector, + kakaoConnector, ]; const findAllConnectors = jest.fn(async () => connectors); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b81b0829e..428b00af5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -422,6 +422,45 @@ importers: prettier: 2.7.1 typescript: 4.7.4 + packages/connector-kakao: + specifiers: + '@jest/types': ^28.1.3 + '@logto/connector-core': ^1.0.0-beta.5 + '@silverhand/eslint-config': 1.0.0-rc.2 + '@silverhand/essentials': ^1.2.0 + '@silverhand/jest-config': 1.0.0-rc.3 + '@silverhand/ts-config': 1.0.0-rc.2 + '@types/jest': ^28.1.6 + '@types/node': ^16.3.1 + eslint: ^8.21.0 + got: ^11.8.2 + jest: ^28.1.3 + jest-matcher-specific-error: ^1.0.0 + lint-staged: ^13.0.0 + nock: ^13.2.2 + prettier: ^2.7.1 + typescript: ^4.7.4 + zod: ^3.14.3 + dependencies: + '@logto/connector-core': link:../connector-core + '@silverhand/essentials': 1.2.0 + '@silverhand/jest-config': 1.0.0-rc.3_bi2kohzqnxavgozw3csgny5hju + got: 11.8.3 + zod: 3.14.3 + devDependencies: + '@jest/types': 28.1.3 + '@silverhand/eslint-config': 1.0.0-rc.2_swk2g7ygmfleszo5c33j4vooni + '@silverhand/ts-config': 1.0.0-rc.2_typescript@4.7.4 + '@types/jest': 28.1.6 + '@types/node': 16.11.12 + eslint: 8.21.0 + jest: 28.1.3_@types+node@16.11.12 + jest-matcher-specific-error: 1.0.0 + lint-staged: 13.0.0 + nock: 13.2.2 + prettier: 2.7.1 + typescript: 4.7.4 + packages/connector-mock-email: specifiers: '@logto/connector-core': ^1.0.0-beta.6 @@ -828,6 +867,7 @@ importers: '@logto/connector-facebook': ^1.0.0-beta.6 '@logto/connector-github': ^1.0.0-beta.6 '@logto/connector-google': ^1.0.0-beta.6 + '@logto/connector-kakao': ^1.0.0-beta.6 '@logto/connector-sendgrid-email': ^1.0.0-beta.6 '@logto/connector-smtp': ^1.0.0-beta.6 '@logto/connector-twilio-sms': ^1.0.0-beta.6 @@ -913,6 +953,7 @@ importers: '@logto/connector-facebook': link:../connector-facebook '@logto/connector-github': link:../connector-github '@logto/connector-google': link:../connector-google + '@logto/connector-kakao': link:../connector-kakao '@logto/connector-sendgrid-email': link:../connector-sendgrid-mail '@logto/connector-smtp': link:../connector-smtp '@logto/connector-twilio-sms': link:../connector-twilio-sms @@ -2317,6 +2358,7 @@ packages: pacote: 13.4.1 semver: 7.3.7 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2347,6 +2389,7 @@ packages: p-waterfall: 2.1.1 semver: 7.3.7 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2487,6 +2530,7 @@ packages: whatwg-url: 8.7.0 yargs-parser: 20.2.4 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2684,6 +2728,7 @@ packages: npm-registry-fetch: 9.0.0 npmlog: 4.1.2 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2713,6 +2758,7 @@ packages: pify: 5.0.0 read-package-json: 3.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2751,6 +2797,7 @@ packages: npmlog: 4.1.2 tar: 6.1.11 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2849,6 +2896,7 @@ packages: pacote: 13.4.1 semver: 7.3.7 transitivePeerDependencies: + - bluebird - encoding - supports-color dev: true @@ -2894,6 +2942,7 @@ packages: '@npmcli/run-script': 3.0.2 npmlog: 4.1.2 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -2995,6 +3044,7 @@ packages: slash: 3.0.0 write-json-file: 4.3.0 transitivePeerDependencies: + - bluebird - encoding - supports-color dev: true @@ -3224,6 +3274,7 @@ packages: treeverse: 2.0.0 walk-up-path: 1.0.0 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -3259,6 +3310,8 @@ packages: promise-retry: 2.0.1 semver: 7.3.7 which: 2.0.2 + transitivePeerDependencies: + - bluebird dev: true /@npmcli/installed-package-contents/1.0.7: @@ -3289,6 +3342,7 @@ packages: pacote: 13.4.1 semver: 7.3.7 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -3340,6 +3394,7 @@ packages: node-gyp: 9.0.0 read-package-json-fast: 2.0.3 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -5981,6 +6036,8 @@ packages: ssri: 8.0.1 tar: 6.1.11 unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird dev: true /cacache/16.1.0: @@ -6005,6 +6062,8 @@ packages: ssri: 9.0.1 tar: 6.1.11 unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird dev: true /cache-content-type/1.0.1: @@ -10594,6 +10653,7 @@ packages: import-local: 3.1.0 npmlog: 4.1.2 transitivePeerDependencies: + - bluebird - encoding - supports-color dev: true @@ -10627,6 +10687,7 @@ packages: npm-package-arg: 8.1.5 npm-registry-fetch: 11.0.0 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -10640,6 +10701,7 @@ packages: semver: 7.3.7 ssri: 8.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -10962,6 +11024,7 @@ packages: socks-proxy-agent: 6.1.1 ssri: 9.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -10985,6 +11048,7 @@ packages: socks-proxy-agent: 5.0.1 ssri: 8.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -11009,6 +11073,7 @@ packages: socks-proxy-agent: 6.1.1 ssri: 8.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -11901,6 +11966,7 @@ packages: tar: 6.1.11 which: 2.0.2 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -12086,6 +12152,7 @@ packages: minizlib: 2.1.2 npm-package-arg: 8.1.5 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -12101,6 +12168,7 @@ packages: npm-package-arg: 9.0.2 proc-log: 2.0.1 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -12117,6 +12185,7 @@ packages: minizlib: 2.1.2 npm-package-arg: 8.1.5 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -12517,6 +12586,7 @@ packages: ssri: 9.0.1 tar: 6.1.11 transitivePeerDependencies: + - bluebird - supports-color dev: true @@ -13246,6 +13316,11 @@ packages: /promise-inflight/1.0.1: resolution: {integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true dev: true /promise-retry/2.0.1: