0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: add GitLab connector (#6529)

This commit is contained in:
DevTekVE 2024-09-02 10:44:45 +02:00 committed by GitHub
parent 479d5895af
commit 31296f0bc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 680 additions and 9 deletions

View file

@ -0,0 +1,18 @@
---
"@logto/connector-gitlab": major
---
add GitLab social connector leveraging OAuth2
### Major Changes
- Initial release of the GitLab connector.
This release introduces the Logto connector for GitLab, enabling social sign-in using GitLab accounts. It supports OAuth 2.0 authentication flow, fetching user information, and handling errors gracefully.
### Features
- **OAuth 2.0 Authentication**: Support for OAuth 2.0 authentication flow with GitLab.
- **User Information Retrieval**: Fetches user details such as full name, email, profile URL, and avatar.
- **Error Handling**: Graceful handling of OAuth errors, including token exchange failures and user-denied permissions.
- **Configurable Scope**: Allows customization of OAuth scopes to access different levels of user information.

View file

@ -0,0 +1,62 @@
# GitLab Connector
The official Logto connector for GitLab social sign-in, based on the Hugging Face connector by Silverhand Inc.
**Table of contents**
- [GitLab connector](#gitlab-connector)
- [Get started](#get-started)
- [Sign in with GitLab account](#sign-in-with-gitlab-account)
- [Create and configure OAuth app](#create-and-configure-oauth-app)
- [Managing OAuth apps](#managing-oauth-apps)
- [Configure your connector](#configure-your-connector)
- [Config types](#config-types)
- [Test GitLab connector](#test-gitlab-connector)
- [Reference](#reference)
## Get started
The GitLab connector enables end-users to sign in to your application using their own GitLab accounts via the GitLab OAuth 2.0 authentication protocol.
## Sign in with GitLab account
Go to the [GitLab website](https://gitlab.com/) and sign in with your GitLab account. You may register a new account if you don't have one.
## Create and configure OAuth app
Follow the [creating a GitLab OAuth App](https://docs.gitlab.com/ee/integration/oauth_provider.html) guide, and register a new application.
Name your new OAuth application in **Name** and fill up **Redirect URI** of the app. Customize the **Redirect URIs** as `${your_logto_origin}/callback/${connector_id}`. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
On scopes, select `openid`. You also may want to enable `profile`, and `email`. `profile` scope is required to get the user's profile information, and `email` scope is required to get the user's email address. Ensure you have allowed these scopes in your GitLab OAuth app if you want to use them.
> Notes:
> * If you use custom domains, add both the custom domain and the default Logto domain to the Redirect URIs to ensure the OAuth flow works correctly with both domains.
> * If you encounter the error message "The redirect_uri MUST match the registered callback URL for this application." when logging in, try aligning the Redirect URI of your GitLab OAuth App and your Logto App's redirect URL (including the protocol) to resolve the issue.
## Managing OAuth apps
Go to the [Applications page](https://gitlab.com/-/profile/applications) on GitLab, where you can add, edit, or delete existing OAuth apps. You can also find the `Application ID` and generate `Secret` in the OAuth app detail pages.
## Configure your connector
Fill out the `clientId` and `clientSecret` field with the _Application ID_ and _Secret_ you've got from the OAuth app detail pages mentioned in the previous section.
`scope` is a space-delimited list of [scopes](https://docs.gitlab.com/ee/integration/oauth_provider.html#authorized-applications). If not provided, scope defaults to be `openid`. For GitLab connector, the scope you may want to use are `openid`, `profile` and `email`. `profile` scope is required to get the user's profile information, and `email` scope is required to get the user's email address. Ensure you have allowed these scopes in your GitLab OAuth app (configured in [Create an OAuth app in the Hugging Face](#create-and-configure-oauth-app) section).
### Config types
| Name | Type |
|--------------|--------|
| clientId | string |
| clientSecret | string |
| scope | string |
## Test GitLab connector
That's it. The GitLab connector should be available now. Don't forget to [Enable connector in sign-in experience](https://docs.logto.io/docs/recipes/configure-connectors/social-connector/enable-social-sign-in/).
## Reference
- [GitLab - API Documentation](https://docs.gitlab.com/ee/api/)
- [GitLab - OAuth Applications](https://docs.gitlab.com/ee/integration/oauth_provider.html)

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="93.97 97.52 192.07 185"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,72 @@
{
"name": "@logto/connector-gitlab",
"version": "0.0.1",
"description": "GitLab social connector implementation.",
"dependencies": {
"@logto/connector-kit": "workspace:^4.0.0",
"@logto/connector-oauth": "workspace:^1.4.0",
"@logto/shared": "workspace:^3.1.1",
"@silverhand/essentials": "^2.9.1",
"jose": "^5.6.3",
"ky": "^1.2.3",
"nanoid": "^5.0.1",
"snakecase-keys": "^8.0.1",
"zod": "^3.23.8"
},
"main": "./lib/index.js",
"module": "./lib/index.js",
"exports": "./lib/index.js",
"license": "MPL-2.0",
"type": "module",
"files": [
"lib",
"docs",
"logo.svg",
"logo-dark.svg"
],
"scripts": {
"precommit": "lint-staged",
"check": "tsc --noEmit",
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"test": "vitest src",
"test:ci": "pnpm run test --silent --coverage",
"prepublishOnly": "pnpm build"
},
"engines": {
"node": "^20.9.0"
},
"eslintConfig": {
"extends": "@silverhand",
"settings": {
"import/core-modules": [
"@silverhand/essentials",
"got",
"nock",
"snakecase-keys",
"zod"
]
}
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/node": "^20.11.20",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^2.0.0",
"eslint": "^8.56.0",
"lint-staged": "^15.0.2",
"nock": "14.0.0-beta.9",
"prettier": "^3.0.0",
"supertest": "^7.0.0",
"tsup": "^8.1.0",
"typescript": "^5.5.3",
"vitest": "^2.0.0"
}
}

View file

@ -0,0 +1,36 @@
import type { ConnectorMetadata } from '@logto/connector-kit';
import { ConnectorPlatform } from '@logto/connector-kit';
import { clientSecretFormItem, clientIdFormItem, scopeFormItem } from '@logto/connector-oauth';
export const jwksUri = 'https://gitlab.com/oauth/discovery/keys';
export const authorizationEndpoint = 'https://gitlab.com/oauth/authorize';
export const userInfoEndpoint = 'https://gitlab.com/oauth/userinfo';
export const tokenEndpoint = 'https://gitlab.com/oauth/token';
export const mandatoryScope = 'openid'; // Always required
export const defaultScopes = [mandatoryScope];
export const defaultMetadata: ConnectorMetadata = {
id: 'gitlab-universal',
target: 'gitlab',
platform: ConnectorPlatform.Universal,
name: {
en: 'GitLab',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'GitLab is an online community for software development and version control.',
},
readme: './README.md',
formItems: [
clientIdFormItem,
clientSecretFormItem,
{
...scopeFormItem,
description:
"`openid` is required to allow OIDC and it's always added to the scopes if not present, `profile` is required to get user's profile information and `email` is required to get user's email address. These scopes can be used individually or in combination; if no scopes are specified, `openid` will be used by default.",
},
],
};
export const defaultTimeout = 5000;

View file

@ -0,0 +1,187 @@
import nock from 'nock';
import { ConnectorError, ConnectorErrorCodes, type SocialUserInfo } from '@logto/connector-kit';
import { authorizationEndpoint, tokenEndpoint, userInfoEndpoint } from './constant.js';
import createConnector from './index.js';
const getConfig = vi.fn().mockResolvedValue({
clientId: '<client-id>',
clientSecret: '<client-secret>',
scope: 'profile email',
});
const getSessionMock = vi.fn().mockResolvedValue({ redirectUri: 'http://localhost:3000/callback' });
describe('GitLab connector', () => {
beforeEach(() => {
nock(tokenEndpoint).post('').reply(200, {
access_token: 'access_token',
scope: 'scope',
token_type: 'token_type',
});
});
afterEach(() => {
nock.cleanAll();
vi.clearAllMocks();
});
it('should get a valid uri by redirectUri and state', async () => {
const connector = await createConnector({ getConfig });
const authorizationUri = await connector.getAuthorizationUri(
{
state: 'some_state',
redirectUri: 'http://localhost:3000/callback',
connectorId: 'some_connector_id',
connectorFactoryId: 'some_connector_factory_id',
jti: 'some_jti',
headers: {},
},
vi.fn()
);
expect(authorizationUri).toEqual(
`${authorizationEndpoint}?${new URLSearchParams({
response_type: 'code',
client_id: '<client-id>',
scope: 'profile email openid', // We add openid to the scopes if not present always
redirect_uri: 'http://localhost:3000/callback',
state: 'some_state',
}).toString()}`
);
});
it('should get valid SocialUserInfo', async () => {
nock(userInfoEndpoint)
.get('')
.reply(200, {
sub: '1234567',
sub_legacy: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
name: 'John Doe',
nickname: 'john.doe',
preferred_username: 'john.doe',
email: 'john.doe@example.com',
email_verified: true,
profile: 'https://gitlab.com/john.doe',
picture: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
groups: ['group1', 'group2', 'group3'],
});
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, getSessionMock);
const expectedSocialUserInfo: SocialUserInfo = {
id: '1234567',
avatar: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
name: 'John Doe',
email: 'john.doe@example.com',
rawData: {
sub: '1234567',
sub_legacy: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
name: 'John Doe',
nickname: 'john.doe',
preferred_username: 'john.doe',
email: 'john.doe@example.com',
email_verified: true,
profile: 'https://gitlab.com/john.doe',
picture: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
groups: ['group1', 'group2', 'group3'],
},
};
expect(socialUserInfo).toStrictEqual(expectedSocialUserInfo);
});
it('should not return email when email_verified is false on SocialUserInfo', async () => {
nock(userInfoEndpoint)
.get('')
.reply(200, {
sub: '1234567',
sub_legacy: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
name: 'John Doe',
nickname: 'john.doe',
preferred_username: 'john.doe',
email: 'john.doe@example.com',
email_verified: false,
profile: 'https://gitlab.com/john.doe',
picture: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
groups: ['group1', 'group2', 'group3'],
});
const connector = await createConnector({ getConfig });
const socialUserInfo = await connector.getUserInfo({ code: 'code' }, getSessionMock);
const expectedSocialUserInfo: SocialUserInfo = {
id: '1234567',
avatar: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
name: 'John Doe',
rawData: {
sub: '1234567',
sub_legacy: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
name: 'John Doe',
nickname: 'john.doe',
preferred_username: 'john.doe',
email: 'john.doe@example.com',
email_verified: false,
profile: 'https://gitlab.com/john.doe',
picture: 'https://gitlab.com/uploads/-/system/user/avatar/1234567/avatar.png',
groups: ['group1', 'group2', 'group3'],
},
};
expect(socialUserInfo).toStrictEqual(expectedSocialUserInfo);
});
it('throws AuthorizationFailed error if authentication failed', async () => {
const connector = await createConnector({ getConfig });
await expect(
connector.getUserInfo({ error: 'some error' }, getSessionMock)
).rejects.toStrictEqual(
new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, { error: 'some error' })
);
});
it('throws InvalidResponse error if token response is invalid', async () => {
// Clear token response mock
nock.cleanAll();
nock(tokenEndpoint).post('').reply(200, {
invalid_field: true,
});
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, getSessionMock)).rejects.toSatisfy(
(connectorError) =>
(connectorError as ConnectorError).code === ConnectorErrorCodes.InvalidResponse
);
});
it('throws InvalidResponse error if userinfo response is invalid', async () => {
nock(userInfoEndpoint).get('').reply(200, {
id: 'id',
});
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, getSessionMock)).rejects.toSatisfy(
(connectorError) =>
(connectorError as ConnectorError).code === ConnectorErrorCodes.InvalidResponse
);
});
it('throws SocialAccessTokenInvalid error if user info responded with 401', async () => {
nock(userInfoEndpoint).get('').reply(401);
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, getSessionMock)).rejects.toStrictEqual(
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
});
it('throws General error if user info responded with a non-401 error', async () => {
nock(userInfoEndpoint).get('').reply(422);
const connector = await createConnector({ getConfig });
await expect(connector.getUserInfo({ code: 'code' }, getSessionMock)).rejects.toStrictEqual(
new ConnectorError(ConnectorErrorCodes.General)
);
});
});

View file

@ -0,0 +1,180 @@
import { assert, conditional } from '@silverhand/essentials';
import type {
GetAuthorizationUri,
GetUserInfo,
SocialConnector,
SocialUserInfo,
CreateConnector,
GetConnectorConfig,
} from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
validateConfig,
ConnectorType,
parseJson,
} from '@logto/connector-kit';
import {
constructAuthorizationUri,
oauth2AuthResponseGuard,
requestTokenEndpoint,
TokenEndpointAuthMethod,
} from '@logto/connector-oauth';
import ky, { HTTPError } from 'ky';
import {
authorizationEndpoint,
mandatoryScope,
defaultScopes,
userInfoEndpoint,
defaultMetadata,
defaultTimeout,
tokenEndpoint,
} from './constant.js';
import { accessTokenResponseGuard, gitlabConfigGuard, userInfoResponseGuard } from './types.js';
/**
* Generates an authorization URI for the given configuration.
*
* @param {GetConnectorConfig} getConfig - Function to retrieve the connector configuration.
* @return {GetAuthorizationUri} - A function that generates the authorization URI.
*/
const getAuthorizationUri =
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
async ({ state, redirectUri }, setSession) => {
const config = await getConfig(defaultMetadata.id);
validateConfig(config, gitlabConfigGuard);
const { clientId, scope } = config;
await setSession({ redirectUri });
const scopes = scope?.split(' ') ?? defaultScopes;
const scopesWithRequired = scopes.includes(mandatoryScope)
? scopes
: [...scopes, mandatoryScope];
return constructAuthorizationUri(authorizationEndpoint, {
responseType: 'code',
clientId,
scope: scopesWithRequired.join(' '), // Defaults to mandatoryScope if not provided
redirectUri,
state,
});
};
/**
* Asynchronously fetches user information from a social media provider.
*
* @param {GetConnectorConfig} getConfig - A function to get the connector configuration.
*
* @returns {GetUserInfo} - A function that retrieves user information.
*
* @throws {ConnectorError} - If an error occurs during the retrieval of user information.
*/
const getUserInfo =
(getConfig: GetConnectorConfig): GetUserInfo =>
async (data, getSession): Promise<SocialUserInfo> => {
const authResponseResult = oauth2AuthResponseGuard.safeParse(data);
if (!authResponseResult.success) {
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, data);
}
const { code } = authResponseResult.data;
const config = await getConfig(defaultMetadata.id);
validateConfig(config, gitlabConfigGuard);
const { clientId, clientSecret } = config;
const { redirectUri } = await getSession();
if (!redirectUri) {
throw new ConnectorError(ConnectorErrorCodes.General, {
message: 'Cannot find `redirectUri` from connector session.',
});
}
const tokenResponse = await requestTokenEndpoint({
tokenEndpoint,
tokenEndpointAuthOptions: {
method: TokenEndpointAuthMethod.ClientSecretBasic,
},
tokenRequestBody: {
grantType: 'authorization_code',
code,
redirectUri,
clientId,
clientSecret,
},
});
const parsedTokenResponse = accessTokenResponseGuard.safeParse(await tokenResponse.json());
if (!parsedTokenResponse.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsedTokenResponse.error);
}
const { access_token: accessToken, token_type: tokenType } = parsedTokenResponse.data;
assert(accessToken, new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid));
try {
const userInfoResponse = await ky.get(userInfoEndpoint, {
headers: {
authorization: `${tokenType} ${accessToken}`,
},
timeout: defaultTimeout,
});
const rawData = parseJson(await userInfoResponse.text());
const parsedUserInfoResponse = userInfoResponseGuard.safeParse(rawData);
if (!parsedUserInfoResponse.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsedUserInfoResponse.error);
}
const { email, email_verified, name, picture: avatar, sub: id } = parsedUserInfoResponse.data;
return {
id,
...conditional(email_verified && { email }),
name,
avatar,
rawData,
};
} catch (error: unknown) {
if (error instanceof HTTPError) {
const { response } = error;
if (response.status === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, await response.text());
}
throw error;
}
};
/**
* Creates a GitLab connector.
*
* @param {Object} options - Options for creating the connector.
* @param {Function} options.getConfig - Function used to get configuration options.
* @returns {Object} - GitLab connector object.
*/
const createGitLabConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Social,
configGuard: gitlabConfigGuard,
getAuthorizationUri: getAuthorizationUri(getConfig),
getUserInfo: getUserInfo(getConfig),
};
};
export default createGitLabConnector;

View file

@ -0,0 +1,4 @@
export const mockedConfig = {
clientId: '<client-id>',
clientSecret: '<client-secret>',
};

View file

@ -0,0 +1,41 @@
import { z } from 'zod';
// Define a configuration guard for GitLab
export const gitlabConfigGuard = z.object({
clientId: z.string(),
clientSecret: z.string(),
scope: z.string().optional(),
});
// Define a guard for validating the access token response
export const accessTokenResponseGuard = z.object({
access_token: z.string(),
token_type: z.string(),
expires_in: z.number().optional(),
refresh_token: z.string().optional(),
scope: z.string(),
});
export type AccessTokenResponse = z.infer<typeof accessTokenResponseGuard>;
// Define a guard for validating the GitLab user info response. Note: things that are not strictly necessary are marked as optional, even though the response does actually contain them.
export const userInfoResponseGuard = z.object({
sub: z.string(),
name: z.string().optional(),
nickname: z.string().optional(),
sub_legacy: z.string().optional(),
preferred_username: z.string().optional(),
email: z.string().optional(),
email_verified: z.boolean().optional(),
profile: z.string().url().optional(),
picture: z.string().url().optional(),
groups: z.array(z.string()).optional(),
});
export const authorizationCallbackErrorGuard = z.object({
error: z.string(),
error_description: z.string(),
error_uri: z.string(),
});
export const authResponseGuard = z.object({ code: z.string() });

View file

@ -945,6 +945,76 @@ importers:
specifier: ^2.0.0
version: 2.0.0(@types/node@20.11.20)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)
packages/connectors/connector-gitlab:
dependencies:
'@logto/connector-kit':
specifier: workspace:^4.0.0
version: link:../../toolkit/connector-kit
'@logto/connector-oauth':
specifier: workspace:^1.4.0
version: link:../connector-oauth2
'@logto/shared':
specifier: workspace:^3.1.1
version: link:../../shared
'@silverhand/essentials':
specifier: ^2.9.1
version: 2.9.1
jose:
specifier: ^5.6.3
version: 5.6.3
ky:
specifier: ^1.2.3
version: 1.2.3
nanoid:
specifier: ^5.0.1
version: 5.0.7
snakecase-keys:
specifier: ^8.0.1
version: 8.0.1
zod:
specifier: ^3.23.8
version: 3.23.8
devDependencies:
'@silverhand/eslint-config':
specifier: 6.0.1
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
'@silverhand/ts-config':
specifier: 6.0.0
version: 6.0.0(typescript@5.5.3)
'@types/node':
specifier: ^20.11.20
version: 20.12.7
'@types/supertest':
specifier: ^6.0.2
version: 6.0.2
'@vitest/coverage-v8':
specifier: ^2.0.0
version: 2.0.0(vitest@2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8))
eslint:
specifier: ^8.56.0
version: 8.57.0
lint-staged:
specifier: ^15.0.2
version: 15.0.2
nock:
specifier: 14.0.0-beta.9
version: 14.0.0-beta.9
prettier:
specifier: ^3.0.0
version: 3.0.0
supertest:
specifier: ^7.0.0
version: 7.0.0
tsup:
specifier: ^8.1.0
version: 8.1.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3))(typescript@5.5.3)
typescript:
specifier: ^5.5.3
version: 5.5.3
vitest:
specifier: ^2.0.0
version: 2.0.0(@types/node@20.12.7)(happy-dom@14.12.3)(jsdom@20.0.2)(lightningcss@1.25.1)(sass@1.77.8)
packages/connectors/connector-google:
dependencies:
'@logto/connector-kit':
@ -15368,10 +15438,10 @@ snapshots:
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-config-xo: 0.44.0(eslint@8.57.0)
eslint-config-xo-typescript: 4.0.0(@typescript-eslint/eslint-plugin@7.7.0(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3))(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0)(typescript@5.5.3)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-consistent-default-export-name: 0.0.15
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-n: 17.2.1(eslint@8.57.0)
eslint-plugin-no-use-extend-native: 0.5.0
eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.0.0)
@ -18316,13 +18386,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0):
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0):
dependencies:
debug: 4.3.5
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@ -18333,14 +18403,14 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7(supports-color@5.5.0)
optionalDependencies:
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.5.3)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
@ -18362,7 +18432,7 @@ snapshots:
eslint: 8.57.0
ignore: 5.3.1
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
@ -18372,7 +18442,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.5.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3