mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor: social connectors (#975)
* refactor: social connectors * refactor(connectors): remove unused fixme comment * fix(connectors): test
This commit is contained in:
parent
ddf113f1db
commit
a686b1707a
28 changed files with 473 additions and 257 deletions
|
@ -39,10 +39,10 @@ describe('getAuthorizationUri', () => {
|
|||
jest
|
||||
.spyOn(alipayNativeMethods, 'getConfig')
|
||||
.mockResolvedValueOnce(mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
const authorizationUri = await alipayNativeMethods.getAuthorizationUri(
|
||||
'dummy-state',
|
||||
'dummy-redirect-uri'
|
||||
);
|
||||
const authorizationUri = await alipayNativeMethods.getAuthorizationUri({
|
||||
state: 'dummy-state',
|
||||
redirectUri: 'dummy-redirect-uri',
|
||||
});
|
||||
expect(authorizationUri).toBe('alipay://?app_id=2021000000000000&state=dummy-state');
|
||||
});
|
||||
});
|
||||
|
@ -124,6 +124,27 @@ describe('getAccessToken', () => {
|
|||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(alipayNativeMethods, 'getConfig')
|
||||
.mockResolvedValueOnce(mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: '3600',
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: '7200', // Expiring time of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
|
@ -149,9 +170,7 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
const { id, name, avatar } = await alipayNativeMethods.getUserInfo({
|
||||
accessToken: 'access_token',
|
||||
});
|
||||
const { id, name, avatar } = await alipayNativeMethods.getUserInfo({ authCode: 'code' });
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
|
@ -173,9 +192,7 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
alipayNativeMethods.getUserInfo({ accessToken: 'wrong_access_token' })
|
||||
).rejects.toMatchError(
|
||||
await expect(alipayNativeMethods.getUserInfo({ authCode: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
@ -196,9 +213,9 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
alipayNativeMethods.getUserInfo({ accessToken: 'wrong_access_token' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.General));
|
||||
await expect(alipayNativeMethods.getUserInfo({ authCode: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
|
@ -219,9 +236,9 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
alipayNativeMethods.getUserInfo({ accessToken: 'access_token' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
await expect(alipayNativeMethods.getUserInfo({ authCode: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with other request errors', async () => {
|
||||
|
@ -230,8 +247,6 @@ describe('getUserInfo', () => {
|
|||
.mockResolvedValueOnce(mockedAlipayNativeConfigWithValidPrivateKey);
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
|
||||
await expect(
|
||||
alipayNativeMethods.getUserInfo({ accessToken: 'access_token' })
|
||||
).rejects.toThrow();
|
||||
await expect(alipayNativeMethods.getUserInfo({ authCode: 'wrong_code' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,11 +8,9 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AccessTokenObject,
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
ValidateConfig,
|
||||
|
@ -22,6 +20,7 @@ import {
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import got from 'got';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
alipayEndpoint,
|
||||
|
@ -41,19 +40,7 @@ import { signingParameters } from './utils';
|
|||
|
||||
export type { AlipayNativeConfig } from './types';
|
||||
|
||||
type CodePayload = {
|
||||
auth_code: string;
|
||||
};
|
||||
|
||||
const parseCodeFromJson = (json: string): string => {
|
||||
try {
|
||||
const { auth_code } = JSON.parse(json) as CodePayload;
|
||||
|
||||
return auth_code;
|
||||
} catch {
|
||||
return json;
|
||||
}
|
||||
};
|
||||
const dataGuard = z.object({ authCode: z.string() });
|
||||
|
||||
export default class AlipayNativeConnector implements SocialConnector {
|
||||
public metadata: ConnectorMetadata = defaultMetadata;
|
||||
|
@ -70,7 +57,7 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, _) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => {
|
||||
const { appId } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({ app_id: appId, state });
|
||||
|
@ -78,7 +65,7 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code): Promise<AccessTokenObject> => {
|
||||
public getAccessToken = async (code: string) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
|
@ -86,7 +73,7 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
version: '1.0',
|
||||
grant_type: 'authorization_code',
|
||||
code: parseCodeFromJson(code),
|
||||
code,
|
||||
charset: 'UTF8',
|
||||
...config,
|
||||
};
|
||||
|
@ -111,9 +98,10 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { authCode } = dataGuard.parse(data);
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const { accessToken } = accessTokenObject;
|
||||
const { accessToken } = await this.getAccessToken(authCode);
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
|
@ -146,11 +134,11 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
sub_msg,
|
||||
sub_code,
|
||||
msg,
|
||||
code,
|
||||
code: responseCode,
|
||||
} = response.alipay_user_info_share_response;
|
||||
|
||||
if (sub_msg || sub_code) {
|
||||
if (code === '20001') {
|
||||
if (responseCode === '20001') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
throw new ConnectorError(ConnectorErrorCodes.General);
|
||||
|
|
|
@ -37,10 +37,10 @@ describe('getAuthorizationUri', () => {
|
|||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
jest.spyOn(alipayMethods, 'getConfig').mockResolvedValueOnce(mockedAlipayConfig);
|
||||
const authorizationUri = await alipayMethods.getAuthorizationUri(
|
||||
'some_state',
|
||||
'http://localhost:3001/callback'
|
||||
);
|
||||
const authorizationUri = await alipayMethods.getAuthorizationUri({
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3001/callback',
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?app_id=2021000000000000&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&scope=auth_user&state=some_state`
|
||||
);
|
||||
|
@ -124,6 +124,27 @@ describe('getAccessToken', () => {
|
|||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(alipayMethods, 'getConfig')
|
||||
.mockResolvedValueOnce(mockedAlipayConfigWithValidPrivateKey);
|
||||
|
||||
const alipayEndpointUrl = new URL(alipayEndpoint);
|
||||
nock(alipayEndpointUrl.origin)
|
||||
.post(alipayEndpointUrl.pathname)
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
alipay_system_oauth_token_response: {
|
||||
user_id: '2088000000000000',
|
||||
access_token: 'access_token',
|
||||
expires_in: '3600',
|
||||
refresh_token: 'refresh_token',
|
||||
re_expires_in: '7200', // Expiring time of refresh token, in seconds
|
||||
},
|
||||
sign: '<signature>',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
|
@ -149,7 +170,7 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
const { id, name, avatar } = await alipayMethods.getUserInfo({ accessToken: 'access_token' });
|
||||
const { id, name, avatar } = await alipayMethods.getUserInfo({ code: 'code' });
|
||||
expect(id).toEqual('2088000000000000');
|
||||
expect(name).toEqual('PlayboyEric');
|
||||
expect(avatar).toEqual('https://www.alipay.com/xxx.jpg');
|
||||
|
@ -171,9 +192,7 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
alipayMethods.getUserInfo({ accessToken: 'wrong_access_token' })
|
||||
).rejects.toMatchError(
|
||||
await expect(alipayMethods.getUserInfo({ code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token')
|
||||
);
|
||||
});
|
||||
|
@ -194,9 +213,9 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(
|
||||
alipayMethods.getUserInfo({ accessToken: 'wrong_access_token' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.General));
|
||||
await expect(alipayMethods.getUserInfo({ code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw with right accessToken but empty userInfo', async () => {
|
||||
|
@ -217,7 +236,7 @@ describe('getUserInfo', () => {
|
|||
sign: '<signature>',
|
||||
});
|
||||
|
||||
await expect(alipayMethods.getUserInfo({ accessToken: 'access_token' })).rejects.toMatchError(
|
||||
await expect(alipayMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
});
|
||||
|
@ -228,6 +247,6 @@ describe('getUserInfo', () => {
|
|||
.mockResolvedValueOnce(mockedAlipayConfigWithValidPrivateKey);
|
||||
nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500);
|
||||
|
||||
await expect(alipayMethods.getUserInfo({ accessToken: 'access_token' })).rejects.toThrow();
|
||||
await expect(alipayMethods.getUserInfo({ code: 'code' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,16 +6,15 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
AccessTokenObject,
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
ValidateConfig,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
|
@ -64,7 +63,7 @@ export default class AlipayConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const { appId: app_id } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const redirect_uri = encodeURI(redirectUri);
|
||||
|
@ -79,7 +78,7 @@ export default class AlipayConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code): Promise<AccessTokenObject> => {
|
||||
public getAccessToken = async (code: string) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const initSearchParameters = {
|
||||
method: methodForAccessToken,
|
||||
|
@ -112,9 +111,11 @@ export default class AlipayConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = codeDataGuard.parse(data);
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
const { accessToken } = accessTokenObject;
|
||||
const { accessToken } = await this.getAccessToken(code);
|
||||
|
||||
assert(
|
||||
accessToken && config,
|
||||
new ConnectorError(ConnectorErrorCodes.InsufficientRequestParameters)
|
||||
|
@ -147,11 +148,11 @@ export default class AlipayConnector implements SocialConnector {
|
|||
sub_msg,
|
||||
sub_code,
|
||||
msg,
|
||||
code,
|
||||
code: responseCode,
|
||||
} = response.alipay_user_info_share_response;
|
||||
|
||||
if (sub_msg || sub_code) {
|
||||
if (code === '20001') {
|
||||
if (responseCode === '20001') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
throw new ConnectorError(ConnectorErrorCodes.General);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GetConnectorConfig } from '@logto/connector-types';
|
||||
import nock from 'nock';
|
||||
|
||||
import AppleConnector from '.';
|
||||
import { authorizationEndpoint } from './constant';
|
||||
|
@ -20,28 +19,16 @@ describe('getAuthorizationUri', () => {
|
|||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const authorizationUri = await appleMethods.getAuthorizationUri(
|
||||
'some_state',
|
||||
'http://localhost:3000/callback'
|
||||
);
|
||||
const authorizationUri = await appleMethods.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&scope=&state=some_state&response_type=code+id_token&response_mode=fragment`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return code directly', async () => {
|
||||
const accessToken = await appleMethods.getAccessToken('code');
|
||||
expect(accessToken).toEqual('code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfig', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
ValidateConfig,
|
||||
GetUserInfo,
|
||||
|
@ -12,10 +11,10 @@ import {
|
|||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
|
||||
import { scope, defaultMetadata, jwksUri, issuer, authorizationEndpoint } from './constant';
|
||||
import { appleConfigGuard, AppleConfig, appleDataGuard } from './types';
|
||||
import { appleConfigGuard, AppleConfig, dataGuard } from './types';
|
||||
|
||||
// TO-DO: support nonce validation
|
||||
export default class AppleConnector implements SocialConnector<string> {
|
||||
export default class AppleConnector implements SocialConnector {
|
||||
public metadata: ConnectorMetadata = defaultMetadata;
|
||||
|
||||
constructor(public readonly getConfig: GetConnectorConfig<AppleConfig>) {}
|
||||
|
@ -28,7 +27,7 @@ export default class AppleConnector implements SocialConnector<string> {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
|
@ -44,15 +43,8 @@ export default class AppleConnector implements SocialConnector<string> {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
// Directly return now. Refactor with connector interface redesign.
|
||||
getAccessToken: GetAccessToken<string> = async (code) => {
|
||||
return code;
|
||||
};
|
||||
|
||||
// Extract data from JSON string.
|
||||
// Refactor with connector interface redesign.
|
||||
public getUserInfo: GetUserInfo<string> = async (data) => {
|
||||
const { id_token: idToken } = appleDataGuard.parse(JSON.parse(data));
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { id_token: idToken } = dataGuard.parse(data);
|
||||
|
||||
if (!idToken) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid);
|
||||
|
|
|
@ -7,8 +7,6 @@ export const appleConfigGuard = z.object({
|
|||
export type AppleConfig = z.infer<typeof appleConfigGuard>;
|
||||
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
|
||||
export const appleDataGuard = z.object({
|
||||
export const dataGuard = z.object({
|
||||
id_token: z.string(),
|
||||
});
|
||||
|
||||
export type AppleData = z.infer<typeof appleDataGuard>;
|
||||
|
|
|
@ -41,7 +41,7 @@ describe('facebook connector', () => {
|
|||
it('should get a valid authorizationUri with redirectUri and state', async () => {
|
||||
const redirectUri = 'http://localhost:3000/callback';
|
||||
const state = 'some_state';
|
||||
const authorizationUri = await facebookMethods.getAuthorizationUri(state, redirectUri);
|
||||
const authorizationUri = await facebookMethods.getAuthorizationUri({ state, redirectUri });
|
||||
|
||||
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
||||
expect(authorizationUri).toEqual(
|
||||
|
@ -93,6 +93,22 @@ describe('facebook connector', () => {
|
|||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(accessTokenEndpoint)
|
||||
.get('')
|
||||
.query({
|
||||
code,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: dummyRedirectUri,
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
|
@ -110,7 +126,10 @@ describe('facebook connector', () => {
|
|||
picture: { data: { url: avatar } },
|
||||
});
|
||||
|
||||
const socialUserInfo = await facebookMethods.getUserInfo({ accessToken: code });
|
||||
const socialUserInfo = await facebookMethods.getUserInfo({
|
||||
code,
|
||||
redirectUri: dummyRedirectUri,
|
||||
});
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1234567890',
|
||||
avatar,
|
||||
|
@ -121,14 +140,16 @@ describe('facebook connector', () => {
|
|||
|
||||
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
|
||||
nock(userInfoEndpoint).get('').query({ fields }).reply(400);
|
||||
await expect(facebookMethods.getUserInfo({ accessToken: code })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
await expect(
|
||||
facebookMethods.getUserInfo({ code, redirectUri: dummyRedirectUri })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(500);
|
||||
await expect(facebookMethods.getUserInfo({ accessToken: code })).rejects.toThrow();
|
||||
await expect(
|
||||
facebookMethods.getUserInfo({ code, redirectUri: dummyRedirectUri })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,12 +7,12 @@ import {
|
|||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
ValidateConfig,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeWithRedirectDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
|
@ -45,7 +45,7 @@ export default class FacebookConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
|
@ -59,7 +59,7 @@ export default class FacebookConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code, redirectUri) => {
|
||||
public getAccessToken = async (code: string, redirectUri: string) => {
|
||||
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
|
||||
this.metadata.id
|
||||
);
|
||||
|
@ -81,8 +81,9 @@ export default class FacebookConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken } = accessTokenObject;
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code, redirectUri } = codeWithRedirectDataGuard.parse(data);
|
||||
const { accessToken } = await this.getAccessToken(code, redirectUri);
|
||||
|
||||
try {
|
||||
const { id, email, name, picture } = await got
|
||||
|
|
|
@ -20,10 +20,10 @@ describe('getAuthorizationUri', () => {
|
|||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const authorizationUri = await githubMethods.getAuthorizationUri(
|
||||
'some_state',
|
||||
'http://localhost:3000/callback'
|
||||
);
|
||||
const authorizationUri = await githubMethods.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&state=some_state&scope=read%3Auser`
|
||||
);
|
||||
|
@ -75,6 +75,14 @@ describe('validateConfig', () => {
|
|||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
|
@ -87,7 +95,7 @@ describe('getUserInfo', () => {
|
|||
name: 'monalisa octocat',
|
||||
email: 'octocat@github.com',
|
||||
});
|
||||
const socialUserInfo = await githubMethods.getUserInfo({ accessToken: 'code' });
|
||||
const socialUserInfo = await githubMethods.getUserInfo({ code: 'code' });
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1',
|
||||
avatar: 'https://github.com/images/error/octocat_happy.gif',
|
||||
|
@ -103,7 +111,7 @@ describe('getUserInfo', () => {
|
|||
name: null,
|
||||
email: null,
|
||||
});
|
||||
const socialUserInfo = await githubMethods.getUserInfo({ accessToken: 'code' });
|
||||
const socialUserInfo = await githubMethods.getUserInfo({ code: 'code' });
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1',
|
||||
});
|
||||
|
@ -111,13 +119,13 @@ describe('getUserInfo', () => {
|
|||
|
||||
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(401);
|
||||
await expect(githubMethods.getUserInfo({ accessToken: 'code' })).rejects.toMatchError(
|
||||
await expect(githubMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpoint).get('').reply(500);
|
||||
await expect(githubMethods.getUserInfo({ accessToken: 'code' })).rejects.toThrow();
|
||||
await expect(githubMethods.getUserInfo({ code: 'code' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
ValidateConfig,
|
||||
GetUserInfo,
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
ConnectorErrorCodes,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert, conditional } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
|
@ -35,7 +35,7 @@ export default class GithubConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
|
@ -48,7 +48,7 @@ export default class GithubConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
getAccessToken: GetAccessToken = async (code) => {
|
||||
getAccessToken = async (code: string) => {
|
||||
const { clientId: client_id, clientSecret: client_secret } = await this.getConfig(
|
||||
this.metadata.id
|
||||
);
|
||||
|
@ -70,8 +70,9 @@ export default class GithubConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken } = accessTokenObject;
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = codeDataGuard.parse(data);
|
||||
const { accessToken } = await this.getAccessToken(code);
|
||||
|
||||
try {
|
||||
const {
|
||||
|
|
|
@ -41,10 +41,10 @@ describe('google connector', () => {
|
|||
});
|
||||
|
||||
it('should get a valid authorizationUri with redirectUri and state', async () => {
|
||||
const authorizationUri = await googleMethods.getAuthorizationUri(
|
||||
'some_state',
|
||||
'http://localhost:3000/callback'
|
||||
);
|
||||
const authorizationUri = await googleMethods.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&scope=openid+profile+email`
|
||||
);
|
||||
|
@ -76,6 +76,14 @@ describe('google connector', () => {
|
|||
});
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
nock(accessTokenEndpoint).post('').reply(200, {
|
||||
access_token: 'access_token',
|
||||
scope: 'scope',
|
||||
token_type: 'token_type',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
|
@ -92,7 +100,7 @@ describe('google connector', () => {
|
|||
email_verified: true,
|
||||
locale: 'en',
|
||||
});
|
||||
const socialUserInfo = await googleMethods.getUserInfo({ accessToken: 'code' });
|
||||
const socialUserInfo = await googleMethods.getUserInfo({ code: 'code', redirectUri: '' });
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: '1234567890',
|
||||
avatar: 'https://github.com/images/error/octocat_happy.gif',
|
||||
|
@ -103,14 +111,14 @@ describe('google connector', () => {
|
|||
|
||||
it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => {
|
||||
nock(userInfoEndpoint).post('').reply(401);
|
||||
await expect(googleMethods.getUserInfo({ accessToken: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
await expect(
|
||||
googleMethods.getUserInfo({ code: 'code', redirectUri: '' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpoint).post('').reply(500);
|
||||
await expect(googleMethods.getUserInfo({ accessToken: 'code' })).rejects.toThrow();
|
||||
await expect(googleMethods.getUserInfo({ code: 'code', redirectUri: '' })).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
import {
|
||||
ConnectorError,
|
||||
ConnectorErrorCodes,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
GetUserInfo,
|
||||
ConnectorMetadata,
|
||||
ValidateConfig,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeWithRedirectDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { conditional, assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
|
@ -39,7 +39,7 @@ export default class GoogleConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const config = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
|
@ -53,7 +53,7 @@ export default class GoogleConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code, redirectUri) => {
|
||||
public getAccessToken = async (code: string, redirectUri: string) => {
|
||||
const { clientId, clientSecret } = await this.getConfig(this.metadata.id);
|
||||
|
||||
// Note:Need to decodeURIComponent on code
|
||||
|
@ -76,8 +76,9 @@ export default class GoogleConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken } = accessTokenObject;
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code, redirectUri } = codeWithRedirectDataGuard.parse(data);
|
||||
const { accessToken } = await this.getAccessToken(code, redirectUri);
|
||||
|
||||
try {
|
||||
const {
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"node": "^16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@logto/phrases": "^0.1.0"
|
||||
"@logto/phrases": "^0.1.0",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
import { Nullable } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum ConnectorType {
|
||||
Email = 'Email',
|
||||
|
@ -86,25 +87,33 @@ export interface EmailConnector extends BaseConnector {
|
|||
sendMessage: EmailSendMessageFunction;
|
||||
}
|
||||
|
||||
export interface SocialConnector<TokenObject = AccessTokenObject> extends BaseConnector {
|
||||
export interface SocialConnector extends BaseConnector {
|
||||
getAuthorizationUri: GetAuthorizationUri;
|
||||
getAccessToken: GetAccessToken<TokenObject>;
|
||||
getUserInfo: GetUserInfo<TokenObject>;
|
||||
getUserInfo: GetUserInfo;
|
||||
}
|
||||
|
||||
export type ValidateConfig<T = Record<string, unknown>> = (config: T) => Promise<void>;
|
||||
|
||||
export type GetAuthorizationUri = (state: string, redirectUri: string) => Promise<string>;
|
||||
export type GetAuthorizationUri = (payload: {
|
||||
state: string;
|
||||
redirectUri: string;
|
||||
}) => Promise<string>;
|
||||
|
||||
export type AccessTokenObject = { accessToken: string } & Record<string, string>;
|
||||
|
||||
export type GetAccessToken<TokenObject = AccessTokenObject> = (
|
||||
code: string,
|
||||
redirectUri?: string
|
||||
) => Promise<TokenObject>;
|
||||
|
||||
export type GetUserInfo<TokenObject = AccessTokenObject> = (
|
||||
accessTokenObject: TokenObject
|
||||
export type GetUserInfo = (
|
||||
data: unknown
|
||||
) => Promise<{ id: string } & Record<string, string | undefined>>;
|
||||
|
||||
export type GetConnectorConfig<T = Record<string, unknown>> = (id: string) => Promise<T>;
|
||||
|
||||
export const codeDataGuard = z.object({
|
||||
code: z.string(),
|
||||
});
|
||||
|
||||
export type CodeData = z.infer<typeof codeDataGuard>;
|
||||
|
||||
export const codeWithRedirectDataGuard = z.object({
|
||||
code: z.string(),
|
||||
redirectUri: z.string(),
|
||||
});
|
||||
|
||||
export type CodeWithRedirectData = z.infer<typeof codeWithRedirectDataGuard>;
|
||||
|
|
|
@ -20,10 +20,10 @@ describe('getAuthorizationUri', () => {
|
|||
});
|
||||
|
||||
it('should get a valid uri', async () => {
|
||||
const authorizationUri = await weChatNativeMethods.getAuthorizationUri(
|
||||
'dummy-state',
|
||||
'dummy-redirect-uri'
|
||||
);
|
||||
const authorizationUri = await weChatNativeMethods.getAuthorizationUri({
|
||||
state: 'dummy-state',
|
||||
redirectUri: 'dummy-redirect-uri',
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?app_id=%3Capp-id%3E&state=dummy-state`
|
||||
);
|
||||
|
@ -82,14 +82,48 @@ describe('validateConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const nockNoOpenIdAccessTokenResponse = () => {
|
||||
const accessTokenEndpointUrl = new URL(accessTokenEndpoint);
|
||||
const parameters = new URLSearchParams({
|
||||
appid: '<app-id>',
|
||||
secret: '<app-secret>',
|
||||
code: 'code',
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
nock(accessTokenEndpointUrl.origin)
|
||||
.get(accessTokenEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
});
|
||||
};
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
const accessTokenEndpointUrl = new URL(accessTokenEndpoint);
|
||||
const parameters = new URLSearchParams({
|
||||
appid: '<app-id>',
|
||||
secret: '<app-secret>',
|
||||
code: 'code',
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
|
||||
nock(accessTokenEndpointUrl.origin)
|
||||
.get(accessTokenEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
openid: 'openid',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const userInfoEndpointUrl = new URL(userInfoEndpoint);
|
||||
const parameters = new URLSearchParams({ access_token: 'accessToken', openid: 'openid' });
|
||||
const parameters = new URLSearchParams({ access_token: 'access_token', openid: 'openid' });
|
||||
|
||||
it('should get valid SocialUserInfo', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
|
@ -97,10 +131,7 @@ describe('getUserInfo', () => {
|
|||
headimgurl: 'https://github.com/images/error/octocat_happy.gif',
|
||||
nickname: 'wechat bot',
|
||||
});
|
||||
const socialUserInfo = await weChatNativeMethods.getUserInfo({
|
||||
accessToken: 'accessToken',
|
||||
openid: 'openid',
|
||||
});
|
||||
const socialUserInfo = await weChatNativeMethods.getUserInfo({ code: 'code' });
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: 'this_is_an_arbitrary_wechat_union_id',
|
||||
avatar: 'https://github.com/images/error/octocat_happy.gif',
|
||||
|
@ -109,14 +140,16 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
|
||||
it('throws error if `openid` is missing', async () => {
|
||||
nock.cleanAll();
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
unionid: 'this_is_an_arbitrary_wechat_union_id',
|
||||
headimgurl: 'https://github.com/images/error/octocat_happy.gif',
|
||||
nickname: 'wechat bot',
|
||||
});
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken' })
|
||||
).rejects.toMatchError(new Error('`openid` is required by WeChat API.'));
|
||||
nockNoOpenIdAccessTokenResponse();
|
||||
await expect(weChatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if errcode is 40001', async () => {
|
||||
|
@ -124,16 +157,14 @@ describe('getUserInfo', () => {
|
|||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, { errcode: 40_001 });
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
await expect(weChatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500);
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toThrow();
|
||||
await expect(weChatNativeMethods.getUserInfo({ code: 'code' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws Error if request failed and errcode is not 40001', async () => {
|
||||
|
@ -141,18 +172,15 @@ describe('getUserInfo', () => {
|
|||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new Error('invalid openid'));
|
||||
await expect(weChatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('invalid openid')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if response code is 401', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(new URLSearchParams({ access_token: 'wrongAccessToken', openid: 'openid' }))
|
||||
.reply(401);
|
||||
await expect(
|
||||
weChatNativeMethods.getUserInfo({ accessToken: 'wrongAccessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401);
|
||||
await expect(weChatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import {
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
ValidateConfig,
|
||||
GetUserInfo,
|
||||
|
@ -13,6 +12,7 @@ import {
|
|||
ConnectorErrorCodes,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
|
@ -47,18 +47,21 @@ export default class WeChatNativeConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, _) => {
|
||||
const { appId } = await this.getConfig(this.metadata.id);
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state }) => {
|
||||
const { appId, universalLinks } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
app_id: appId,
|
||||
state,
|
||||
// `universalLinks` is used by Wechat open platform website,
|
||||
// while `universal_link` is their API requirement.
|
||||
...(universalLinks && { universal_link: universalLinks }),
|
||||
});
|
||||
|
||||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code) => {
|
||||
public getAccessToken = async (code: string) => {
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const {
|
||||
|
@ -80,9 +83,11 @@ export default class WeChatNativeConnector implements SocialConnector {
|
|||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken, openid } = accessTokenObject;
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = codeDataGuard.parse(data);
|
||||
const { accessToken, openid } = await this.getAccessToken(code);
|
||||
|
||||
// TO-DO: @Darcy refactor this
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily have to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In other words, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const weChatNativeConfigGuard = z.object({ appId: z.string(), appSecret: z.string() });
|
||||
export const weChatNativeConfigGuard = z.object({
|
||||
appId: z.string(),
|
||||
appSecret: z.string(),
|
||||
universalLinks: z.string().optional(),
|
||||
});
|
||||
|
||||
export type WeChatNativeConfig = z.infer<typeof weChatNativeConfigGuard>;
|
||||
|
||||
|
|
|
@ -20,10 +20,10 @@ describe('getAuthorizationUri', () => {
|
|||
});
|
||||
|
||||
it('should get a valid uri by redirectUri and state', async () => {
|
||||
const authorizationUri = await weChatMethods.getAuthorizationUri(
|
||||
'some_state',
|
||||
'http://localhost:3001/callback'
|
||||
);
|
||||
const authorizationUri = await weChatMethods.getAuthorizationUri({
|
||||
state: 'some_state',
|
||||
redirectUri: 'http://localhost:3001/callback',
|
||||
});
|
||||
expect(authorizationUri).toEqual(
|
||||
`${authorizationEndpoint}?appid=%3Capp-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&response_type=code&scope=snsapi_login&state=some_state`
|
||||
);
|
||||
|
@ -82,14 +82,47 @@ describe('validateConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const nockNoOpenIdAccessTokenResponse = () => {
|
||||
const accessTokenEndpointUrl = new URL(accessTokenEndpoint);
|
||||
const parameters = new URLSearchParams({
|
||||
appid: '<app-id>',
|
||||
secret: '<app-secret>',
|
||||
code: 'code',
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
nock(accessTokenEndpointUrl.origin)
|
||||
.get(accessTokenEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
});
|
||||
};
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
beforeEach(() => {
|
||||
const accessTokenEndpointUrl = new URL(accessTokenEndpoint);
|
||||
const parameters = new URLSearchParams({
|
||||
appid: '<app-id>',
|
||||
secret: '<app-secret>',
|
||||
code: 'code',
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
nock(accessTokenEndpointUrl.origin)
|
||||
.get(accessTokenEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, {
|
||||
access_token: 'access_token',
|
||||
openid: 'openid',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const userInfoEndpointUrl = new URL(userInfoEndpoint);
|
||||
const parameters = new URLSearchParams({ access_token: 'accessToken', openid: 'openid' });
|
||||
const parameters = new URLSearchParams({ access_token: 'access_token', openid: 'openid' });
|
||||
|
||||
it('should get valid SocialUserInfo', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
|
@ -98,8 +131,7 @@ describe('getUserInfo', () => {
|
|||
nickname: 'wechat bot',
|
||||
});
|
||||
const socialUserInfo = await weChatMethods.getUserInfo({
|
||||
accessToken: 'accessToken',
|
||||
openid: 'openid',
|
||||
code: 'code',
|
||||
});
|
||||
expect(socialUserInfo).toMatchObject({
|
||||
id: 'this_is_an_arbitrary_wechat_union_id',
|
||||
|
@ -109,13 +141,15 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
|
||||
it('throws error if `openid` is missing', async () => {
|
||||
nock.cleanAll();
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, {
|
||||
unionid: 'this_is_an_arbitrary_wechat_union_id',
|
||||
headimgurl: 'https://github.com/images/error/octocat_happy.gif',
|
||||
nickname: 'wechat bot',
|
||||
});
|
||||
await expect(weChatMethods.getUserInfo({ accessToken: 'accessToken' })).rejects.toMatchError(
|
||||
new Error('`openid` is required by WeChat API.')
|
||||
nockNoOpenIdAccessTokenResponse();
|
||||
await expect(weChatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -124,16 +158,14 @@ describe('getUserInfo', () => {
|
|||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, { errcode: 40_001 });
|
||||
await expect(
|
||||
weChatMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
await expect(weChatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws unrecognized error', async () => {
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500);
|
||||
await expect(
|
||||
weChatMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toThrow();
|
||||
await expect(weChatMethods.getUserInfo({ code: 'code' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws Error if request failed and errcode is not 40001', async () => {
|
||||
|
@ -141,18 +173,15 @@ describe('getUserInfo', () => {
|
|||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(parameters)
|
||||
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
|
||||
await expect(
|
||||
weChatMethods.getUserInfo({ accessToken: 'accessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new Error('invalid openid'));
|
||||
await expect(weChatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('invalid openid')
|
||||
);
|
||||
});
|
||||
|
||||
it('throws SocialAccessTokenInvalid error if response code is 401', async () => {
|
||||
nock(userInfoEndpointUrl.origin)
|
||||
.get(userInfoEndpointUrl.pathname)
|
||||
.query(new URLSearchParams({ access_token: 'wrongAccessToken', openid: 'openid' }))
|
||||
.reply(401);
|
||||
await expect(
|
||||
weChatMethods.getUserInfo({ accessToken: 'wrongAccessToken', openid: 'openid' })
|
||||
).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid));
|
||||
nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401);
|
||||
await expect(weChatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import {
|
||||
ConnectorMetadata,
|
||||
GetAccessToken,
|
||||
GetAuthorizationUri,
|
||||
ValidateConfig,
|
||||
GetUserInfo,
|
||||
|
@ -13,6 +12,7 @@ import {
|
|||
ConnectorErrorCodes,
|
||||
SocialConnector,
|
||||
GetConnectorConfig,
|
||||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
|
@ -43,7 +43,7 @@ export default class WeChatConnector implements SocialConnector {
|
|||
}
|
||||
};
|
||||
|
||||
public getAuthorizationUri: GetAuthorizationUri = async (state, redirectUri) => {
|
||||
public getAuthorizationUri: GetAuthorizationUri = async ({ state, redirectUri }) => {
|
||||
const { appId } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
|
@ -57,7 +57,7 @@ export default class WeChatConnector implements SocialConnector {
|
|||
return `${authorizationEndpoint}?${queryParameters.toString()}`;
|
||||
};
|
||||
|
||||
public getAccessToken: GetAccessToken = async (code) => {
|
||||
public getAccessToken = async (code: string) => {
|
||||
const { appId: appid, appSecret: secret } = await this.getConfig(this.metadata.id);
|
||||
|
||||
const {
|
||||
|
@ -79,9 +79,11 @@ export default class WeChatConnector implements SocialConnector {
|
|||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
public getUserInfo: GetUserInfo = async (accessTokenObject) => {
|
||||
const { accessToken, openid } = accessTokenObject;
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = codeDataGuard.parse(data);
|
||||
const { accessToken, openid } = await this.getAccessToken(code);
|
||||
|
||||
// TO-DO: @Darcy refactor this
|
||||
// 'openid' is defined as a required input argument in WeChat API doc, but it does not necessarily have to
|
||||
// be the return value from getAccessToken per testing.
|
||||
// In other words, 'openid' is required but the response of getUserInfo is consistent as long as
|
||||
|
|
|
@ -37,19 +37,11 @@ const getConnector = async (connectorId: string) => {
|
|||
|
||||
export const getUserInfoByAuthCode = async (
|
||||
connectorId: string,
|
||||
authCode: string,
|
||||
redirectUri: string
|
||||
data: unknown
|
||||
): Promise<SocialUserInfo> => {
|
||||
// TO-DO: rename and refactor connector methods
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = JSON.parse(authCode);
|
||||
const connector = await getConnector(connectorId);
|
||||
const accessTokenObject = await connector.getAccessToken(
|
||||
Object.keys(data).length > 1 ? authCode : data.code,
|
||||
redirectUri
|
||||
);
|
||||
|
||||
return connector.getUserInfo(accessTokenObject);
|
||||
return connector.getUserInfo(data);
|
||||
};
|
||||
|
||||
export const getUserInfoFromInteractionResult = async (
|
||||
|
|
|
@ -42,7 +42,7 @@ jest.mock('@/lib/social', () => ({
|
|||
async findSocialRelatedUser() {
|
||||
return ['phone', { id: 'user1', identities: {} }];
|
||||
},
|
||||
async getUserInfoByAuthCode(connectorId: string, authCode: string) {
|
||||
async getUserInfoByAuthCode(connectorId: string, data: { code: string }) {
|
||||
if (connectorId === '_connectorId') {
|
||||
throw new RequestError({
|
||||
code: 'session.invalid_connector_id',
|
||||
|
@ -51,7 +51,7 @@ jest.mock('@/lib/social', () => ({
|
|||
});
|
||||
}
|
||||
|
||||
if (authCode === '123456') {
|
||||
if (data.code === '123456') {
|
||||
return { id: 'id' };
|
||||
}
|
||||
|
||||
|
@ -410,9 +410,11 @@ describe('sessionRoutes', () => {
|
|||
it('get and add user info with auth code, as well as assign result and redirect', async () => {
|
||||
const response = await sessionRequest.post('/session/sign-in/social/auth').send({
|
||||
connectorId: 'connectorId',
|
||||
state: 'state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
code: '123456',
|
||||
data: {
|
||||
state: 'state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
code: '123456',
|
||||
},
|
||||
});
|
||||
expect(updateUserById).toHaveBeenCalledWith(
|
||||
'id',
|
||||
|
@ -432,9 +434,11 @@ describe('sessionRoutes', () => {
|
|||
it('throw error when identity exists', async () => {
|
||||
const response = await sessionRequest.post('/session/sign-in/social/auth').send({
|
||||
connectorId: '_connectorId_',
|
||||
state: 'state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
code: '123456',
|
||||
data: {
|
||||
state: 'state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
code: '123456',
|
||||
},
|
||||
});
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import pick from 'lodash.pick';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
import { object, string, unknown } from 'zod';
|
||||
|
||||
import { getSocialConnectorInstanceById } from '@/connectors';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
@ -196,7 +196,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
assertThat(state && redirectUri, 'session.insufficient_info');
|
||||
const connector = await getSocialConnectorInstanceById(connectorId);
|
||||
assertThat(connector.connector.enabled, 'connector.not_enabled');
|
||||
const redirectTo = await connector.getAuthorizationUri(state, redirectUri);
|
||||
const redirectTo = await connector.getAuthorizationUri({ state, redirectUri });
|
||||
ctx.body = { redirectTo };
|
||||
|
||||
return next();
|
||||
|
@ -208,16 +208,15 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
koaGuard({
|
||||
body: object({
|
||||
connectorId: string(),
|
||||
code: string(),
|
||||
redirectUri: string().regex(redirectUriRegEx),
|
||||
data: unknown(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { connectorId, code, redirectUri } = ctx.guard.body;
|
||||
const { connectorId, data } = ctx.guard.body;
|
||||
const type = 'SignInSocial';
|
||||
ctx.log(type, { connectorId, code, redirectUri });
|
||||
ctx.log(type, { connectorId, data });
|
||||
|
||||
const userInfo = await getUserInfoByAuthCode(connectorId, code, redirectUri);
|
||||
const userInfo = await getUserInfoByAuthCode(connectorId, data);
|
||||
ctx.log(type, { userInfo });
|
||||
|
||||
if (!(await hasUserWithIdentity(connectorId, userInfo.id))) {
|
||||
|
|
|
@ -193,8 +193,10 @@ describe('api', () => {
|
|||
it('signInWithSocial', async () => {
|
||||
const parameters = {
|
||||
connectorId: 'connectorId',
|
||||
redirectUri: 'redirectUri',
|
||||
code: 'code',
|
||||
data: {
|
||||
redirectUri: 'redirectUri',
|
||||
code: 'code',
|
||||
},
|
||||
};
|
||||
await signInWithSocial(parameters);
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/social/auth', {
|
||||
|
|
|
@ -20,18 +20,14 @@ export const invokeSocialSignIn = async (
|
|||
.json<Response>();
|
||||
};
|
||||
|
||||
export const signInWithSocial = async (parameters: {
|
||||
connectorId: string;
|
||||
redirectUri: string;
|
||||
code: string;
|
||||
}) => {
|
||||
export const signInWithSocial = async (json: { connectorId: string; data: unknown }) => {
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
return api
|
||||
.post('/api/session/sign-in/social/auth', {
|
||||
json: parameters,
|
||||
json,
|
||||
})
|
||||
.json<Response>();
|
||||
};
|
||||
|
|
|
@ -37,11 +37,13 @@ const useSocialSignInListener = () => {
|
|||
);
|
||||
|
||||
const signInWithSocialHandler = useCallback(
|
||||
async (connectorId: string, code: string) => {
|
||||
async (connectorId: string, data: Record<string, unknown>) => {
|
||||
void asyncSignInWithSocial({
|
||||
connectorId,
|
||||
code,
|
||||
redirectUri: `${window.location.origin}/callback/${connectorId}`, // For validation use only
|
||||
data: {
|
||||
redirectUri: `${window.location.origin}/callback/${connectorId}`, // For validation use only
|
||||
...data,
|
||||
},
|
||||
});
|
||||
},
|
||||
[asyncSignInWithSocial]
|
||||
|
@ -67,7 +69,7 @@ const useSocialSignInListener = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
void signInWithSocialHandler(parameters.connector, JSON.stringify(rest));
|
||||
void signInWithSocialHandler(parameters.connector, rest);
|
||||
}, [parameters.connector, setToast, signInWithSocialHandler, t]);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ export const parseQueryParameters = (parameters: string | URLSearchParams) => {
|
|||
const searchParameters =
|
||||
parameters instanceof URLSearchParams ? parameters : new URLSearchParams(parameters);
|
||||
|
||||
return Object.fromEntries(searchParameters);
|
||||
return Object.fromEntries(
|
||||
[...searchParameters.entries()].map(([key, value]) => [key, decodeURIComponent(value)])
|
||||
);
|
||||
};
|
||||
|
||||
export const queryStringify = (parameters: URLSearchParams | Record<string, string>) => {
|
||||
|
|
115
pnpm-lock.yaml
generated
115
pnpm-lock.yaml
generated
|
@ -516,8 +516,10 @@ importers:
|
|||
prettier: ^2.3.2
|
||||
ts-jest: ^27.1.1
|
||||
typescript: ^4.6.2
|
||||
zod: ^3.14.3
|
||||
dependencies:
|
||||
'@logto/phrases': link:../phrases
|
||||
zod: 3.14.3
|
||||
devDependencies:
|
||||
'@jest/types': 27.5.1
|
||||
'@shopify/jest-koa-mocks': 4.0.0
|
||||
|
@ -3889,6 +3891,7 @@ packages:
|
|||
pacote: 13.4.1
|
||||
semver: 7.3.7
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -3919,6 +3922,7 @@ packages:
|
|||
p-waterfall: 2.1.1
|
||||
semver: 7.3.7
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4059,6 +4063,7 @@ packages:
|
|||
whatwg-url: 8.7.0
|
||||
yargs-parser: 20.2.4
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4256,6 +4261,7 @@ packages:
|
|||
npm-registry-fetch: 9.0.0
|
||||
npmlog: 4.1.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4285,6 +4291,7 @@ packages:
|
|||
pify: 5.0.0
|
||||
read-package-json: 3.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4323,6 +4330,7 @@ packages:
|
|||
npmlog: 4.1.2
|
||||
tar: 6.1.11
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4421,6 +4429,7 @@ packages:
|
|||
pacote: 13.4.1
|
||||
semver: 7.3.7
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -4466,6 +4475,7 @@ packages:
|
|||
'@npmcli/run-script': 3.0.2
|
||||
npmlog: 4.1.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4567,6 +4577,7 @@ packages:
|
|||
slash: 3.0.0
|
||||
write-json-file: 4.3.0
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -4750,6 +4761,7 @@ packages:
|
|||
treeverse: 2.0.0
|
||||
walk-up-path: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4785,6 +4797,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:
|
||||
|
@ -4815,6 +4829,7 @@ packages:
|
|||
pacote: 13.4.1
|
||||
semver: 7.3.7
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -4866,6 +4881,7 @@ packages:
|
|||
node-gyp: 9.0.0
|
||||
read-package-json-fast: 2.0.3
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -5856,6 +5872,7 @@ packages:
|
|||
stylelint-config-xo-scss: 0.15.0_zhymizk4kfitko2u2d4p3qwyee
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
- eslint-import-resolver-webpack
|
||||
- postcss
|
||||
- prettier
|
||||
- supports-color
|
||||
|
@ -5879,7 +5896,7 @@ packages:
|
|||
eslint-import-resolver-typescript: 2.5.0_rnagsyfcubvpoxo2ynj23pim7u
|
||||
eslint-plugin-consistent-default-export-name: 0.0.7
|
||||
eslint-plugin-eslint-comments: 3.2.0_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_sidoke6kqbkbdht6nlmwbfnany
|
||||
eslint-plugin-no-use-extend-native: 0.5.0
|
||||
eslint-plugin-node: 11.1.0_eslint@8.10.0
|
||||
eslint-plugin-prettier: 3.4.1_6pitu4b2tqihty6rv5qeiyb35m
|
||||
|
@ -5889,6 +5906,7 @@ packages:
|
|||
pkg-dir: 4.2.0
|
||||
prettier: 2.5.1
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
@ -5910,7 +5928,7 @@ packages:
|
|||
eslint-import-resolver-typescript: 2.5.0_rnagsyfcubvpoxo2ynj23pim7u
|
||||
eslint-plugin-consistent-default-export-name: 0.0.7
|
||||
eslint-plugin-eslint-comments: 3.2.0_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_sidoke6kqbkbdht6nlmwbfnany
|
||||
eslint-plugin-no-use-extend-native: 0.5.0
|
||||
eslint-plugin-node: 11.1.0_eslint@8.10.0
|
||||
eslint-plugin-prettier: 3.4.1_6pitu4b2tqihty6rv5qeiyb35m
|
||||
|
@ -5920,6 +5938,7 @@ packages:
|
|||
pkg-dir: 4.2.0
|
||||
prettier: 2.5.1
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
@ -5941,7 +5960,7 @@ packages:
|
|||
eslint-import-resolver-typescript: 2.5.0_rnagsyfcubvpoxo2ynj23pim7u
|
||||
eslint-plugin-consistent-default-export-name: 0.0.7
|
||||
eslint-plugin-eslint-comments: 3.2.0_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_sidoke6kqbkbdht6nlmwbfnany
|
||||
eslint-plugin-no-use-extend-native: 0.5.0
|
||||
eslint-plugin-node: 11.1.0_eslint@8.10.0
|
||||
eslint-plugin-prettier: 3.4.1_6pitu4b2tqihty6rv5qeiyb35m
|
||||
|
@ -5951,6 +5970,7 @@ packages:
|
|||
pkg-dir: 4.2.0
|
||||
prettier: 2.5.1
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
@ -8020,6 +8040,8 @@ packages:
|
|||
qs: 6.9.7
|
||||
raw-body: 2.4.3
|
||||
type-is: 1.6.18
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/bonjour-service/1.0.11:
|
||||
|
@ -8202,6 +8224,8 @@ packages:
|
|||
ssri: 8.0.1
|
||||
tar: 6.1.11
|
||||
unique-filename: 1.1.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
dev: true
|
||||
|
||||
/cacache/16.1.0:
|
||||
|
@ -8226,6 +8250,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:
|
||||
|
@ -8758,6 +8784,8 @@ packages:
|
|||
on-headers: 1.0.2
|
||||
safe-buffer: 5.1.2
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/concat-map/0.0.1:
|
||||
|
@ -9307,12 +9335,22 @@ packages:
|
|||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: true
|
||||
|
||||
/debug/3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
|
@ -9540,6 +9578,8 @@ packages:
|
|||
dependencies:
|
||||
address: 1.1.2
|
||||
debug: 2.6.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/detect-port/1.3.0:
|
||||
|
@ -9549,6 +9589,8 @@ packages:
|
|||
dependencies:
|
||||
address: 1.1.2
|
||||
debug: 2.6.9
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/dezalgo/1.0.3:
|
||||
|
@ -10020,6 +10062,8 @@ packages:
|
|||
dependencies:
|
||||
debug: 3.2.7
|
||||
resolve: 1.22.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-typescript/2.5.0_rnagsyfcubvpoxo2ynj23pim7u:
|
||||
|
@ -10031,7 +10075,7 @@ packages:
|
|||
dependencies:
|
||||
debug: 4.3.3
|
||||
eslint: 8.10.0
|
||||
eslint-plugin-import: 2.25.4_eslint@8.10.0
|
||||
eslint-plugin-import: 2.25.4_sidoke6kqbkbdht6nlmwbfnany
|
||||
glob: 7.2.0
|
||||
is-glob: 4.0.3
|
||||
resolve: 1.22.0
|
||||
|
@ -10040,12 +10084,31 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-module-utils/2.7.3:
|
||||
/eslint-module-utils/2.7.3_l62aq42yiamaj3cnpuf6avthf4:
|
||||
resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint-import-resolver-node: '*'
|
||||
eslint-import-resolver-typescript: '*'
|
||||
eslint-import-resolver-webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
eslint-import-resolver-typescript:
|
||||
optional: true
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.14.0_fo4uz55zgcu432252zy2gvpvcu
|
||||
debug: 3.2.7
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-import-resolver-typescript: 2.5.0_rnagsyfcubvpoxo2ynj23pim7u
|
||||
find-up: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-consistent-default-export-name/0.0.7:
|
||||
|
@ -10080,19 +10143,24 @@ packages:
|
|||
ignore: 5.2.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-import/2.25.4_eslint@8.10.0:
|
||||
/eslint-plugin-import/2.25.4_sidoke6kqbkbdht6nlmwbfnany:
|
||||
resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.14.0_fo4uz55zgcu432252zy2gvpvcu
|
||||
array-includes: 3.1.4
|
||||
array.prototype.flat: 1.2.5
|
||||
debug: 2.6.9
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.10.0
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
eslint-module-utils: 2.7.3
|
||||
eslint-module-utils: 2.7.3_l62aq42yiamaj3cnpuf6avthf4
|
||||
has: 1.0.3
|
||||
is-core-module: 2.8.1
|
||||
is-glob: 4.0.3
|
||||
|
@ -10100,6 +10168,10 @@ packages:
|
|||
object.values: 1.1.5
|
||||
resolve: 1.22.0
|
||||
tsconfig-paths: 3.13.0
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-no-use-extend-native/0.5.0:
|
||||
|
@ -10491,6 +10563,8 @@ packages:
|
|||
type-is: 1.6.18
|
||||
utils-merge: 1.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/extend-shallow/2.0.1:
|
||||
|
@ -10682,6 +10756,8 @@ packages:
|
|||
parseurl: 1.3.3
|
||||
statuses: 1.5.0
|
||||
unpipe: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/find-cache-dir/3.3.2:
|
||||
|
@ -13388,6 +13464,7 @@ packages:
|
|||
import-local: 3.1.0
|
||||
npmlog: 4.1.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -13422,6 +13499,7 @@ packages:
|
|||
npm-package-arg: 8.1.5
|
||||
npm-registry-fetch: 11.0.0
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -13435,6 +13513,7 @@ packages:
|
|||
semver: 7.3.7
|
||||
ssri: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -13865,6 +13944,7 @@ packages:
|
|||
socks-proxy-agent: 6.1.1
|
||||
ssri: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -13888,6 +13968,7 @@ packages:
|
|||
socks-proxy-agent: 5.0.1
|
||||
ssri: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -13912,6 +13993,7 @@ packages:
|
|||
socks-proxy-agent: 6.1.1
|
||||
ssri: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -14816,6 +14898,7 @@ packages:
|
|||
tar: 6.1.11
|
||||
which: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -14987,6 +15070,7 @@ packages:
|
|||
minizlib: 2.1.2
|
||||
npm-package-arg: 8.1.5
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -15002,6 +15086,7 @@ packages:
|
|||
npm-package-arg: 9.0.2
|
||||
proc-log: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -15018,6 +15103,7 @@ packages:
|
|||
minizlib: 2.1.2
|
||||
npm-package-arg: 8.1.5
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -15443,6 +15529,7 @@ packages:
|
|||
ssri: 9.0.1
|
||||
tar: 6.1.11
|
||||
transitivePeerDependencies:
|
||||
- bluebird
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
|
@ -15852,6 +15939,8 @@ packages:
|
|||
async: 2.6.3
|
||||
debug: 3.2.7
|
||||
mkdirp: 0.5.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/postcss-calc/8.2.4_postcss@8.4.12:
|
||||
|
@ -16551,6 +16640,11 @@ packages:
|
|||
|
||||
/promise-inflight/1.0.1:
|
||||
resolution: {integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=}
|
||||
peerDependencies:
|
||||
bluebird: '*'
|
||||
peerDependenciesMeta:
|
||||
bluebird:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/promise-retry/2.0.1:
|
||||
|
@ -16823,6 +16917,7 @@ packages:
|
|||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- eslint
|
||||
- supports-color
|
||||
- typescript
|
||||
- vue-template-compiler
|
||||
- webpack
|
||||
|
@ -17901,6 +17996,8 @@ packages:
|
|||
on-finished: 2.3.0
|
||||
range-parser: 1.2.1
|
||||
statuses: 1.5.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serialize-error/7.0.1:
|
||||
|
@ -17947,6 +18044,8 @@ packages:
|
|||
http-errors: 1.6.3
|
||||
mime-types: 2.1.35
|
||||
parseurl: 1.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/serve-static/1.14.2:
|
||||
|
@ -17957,6 +18056,8 @@ packages:
|
|||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.17.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/set-blocking/2.0.0:
|
||||
|
|
Loading…
Add table
Reference in a new issue