From 31296f0bc8bef835c4f293f954509754df469595 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Mon, 2 Sep 2024 10:44:45 +0200 Subject: [PATCH] feat: add GitLab connector (#6529) --- .changeset/fast-adults-dream.md | 18 ++ .../connectors/connector-gitlab/README.md | 62 ++++++ packages/connectors/connector-gitlab/logo.svg | 1 + .../connectors/connector-gitlab/package.json | 72 +++++++ .../connector-gitlab/src/constant.ts | 36 ++++ .../connector-gitlab/src/index.test.ts | 187 ++++++++++++++++++ .../connectors/connector-gitlab/src/index.ts | 180 +++++++++++++++++ .../connectors/connector-gitlab/src/mock.ts | 4 + .../connectors/connector-gitlab/src/types.ts | 41 ++++ pnpm-lock.yaml | 88 ++++++++- 10 files changed, 680 insertions(+), 9 deletions(-) create mode 100644 .changeset/fast-adults-dream.md create mode 100644 packages/connectors/connector-gitlab/README.md create mode 100644 packages/connectors/connector-gitlab/logo.svg create mode 100644 packages/connectors/connector-gitlab/package.json create mode 100644 packages/connectors/connector-gitlab/src/constant.ts create mode 100644 packages/connectors/connector-gitlab/src/index.test.ts create mode 100644 packages/connectors/connector-gitlab/src/index.ts create mode 100644 packages/connectors/connector-gitlab/src/mock.ts create mode 100644 packages/connectors/connector-gitlab/src/types.ts diff --git a/.changeset/fast-adults-dream.md b/.changeset/fast-adults-dream.md new file mode 100644 index 000000000..ed468998b --- /dev/null +++ b/.changeset/fast-adults-dream.md @@ -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. diff --git a/packages/connectors/connector-gitlab/README.md b/packages/connectors/connector-gitlab/README.md new file mode 100644 index 000000000..8d3533ffa --- /dev/null +++ b/packages/connectors/connector-gitlab/README.md @@ -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) \ No newline at end of file diff --git a/packages/connectors/connector-gitlab/logo.svg b/packages/connectors/connector-gitlab/logo.svg new file mode 100644 index 000000000..def154b19 --- /dev/null +++ b/packages/connectors/connector-gitlab/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/connectors/connector-gitlab/package.json b/packages/connectors/connector-gitlab/package.json new file mode 100644 index 000000000..1c39c73ff --- /dev/null +++ b/packages/connectors/connector-gitlab/package.json @@ -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" + } +} diff --git a/packages/connectors/connector-gitlab/src/constant.ts b/packages/connectors/connector-gitlab/src/constant.ts new file mode 100644 index 000000000..8e193f462 --- /dev/null +++ b/packages/connectors/connector-gitlab/src/constant.ts @@ -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; diff --git a/packages/connectors/connector-gitlab/src/index.test.ts b/packages/connectors/connector-gitlab/src/index.test.ts new file mode 100644 index 000000000..81e6599e9 --- /dev/null +++ b/packages/connectors/connector-gitlab/src/index.test.ts @@ -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: '', + clientSecret: '', + 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: '', + 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) + ); + }); +}); diff --git a/packages/connectors/connector-gitlab/src/index.ts b/packages/connectors/connector-gitlab/src/index.ts new file mode 100644 index 000000000..ae4d3fa01 --- /dev/null +++ b/packages/connectors/connector-gitlab/src/index.ts @@ -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 => { + 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 = async ({ getConfig }) => { + return { + metadata: defaultMetadata, + type: ConnectorType.Social, + configGuard: gitlabConfigGuard, + getAuthorizationUri: getAuthorizationUri(getConfig), + getUserInfo: getUserInfo(getConfig), + }; +}; + +export default createGitLabConnector; diff --git a/packages/connectors/connector-gitlab/src/mock.ts b/packages/connectors/connector-gitlab/src/mock.ts new file mode 100644 index 000000000..a52d77ccc --- /dev/null +++ b/packages/connectors/connector-gitlab/src/mock.ts @@ -0,0 +1,4 @@ +export const mockedConfig = { + clientId: '', + clientSecret: '', +}; diff --git a/packages/connectors/connector-gitlab/src/types.ts b/packages/connectors/connector-gitlab/src/types.ts new file mode 100644 index 000000000..aa4435cc5 --- /dev/null +++ b/packages/connectors/connector-gitlab/src/types.ts @@ -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; + +// 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() }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 116920cef..3f954bc57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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