From 57d97a4df82c6a5a7132c4cc3b978557b8d5c308 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 19 Mar 2024 14:05:42 +0800 Subject: [PATCH] feat: return and store connector raw data --- .changeset/gentle-shoes-push.md | 8 + .changeset/new-tables-breathe.md | 7 + .changeset/olive-cycles-sip.md | 21 + .changeset/wild-hotels-drop.md | 5 + .gitpod.yml | 2 +- package.json | 2 +- .../connector-alipay-native/src/index.test.ts | 15 +- .../connector-alipay-native/src/index.ts | 6 +- .../connector-alipay-web/src/index.test.ts | 15 +- .../connector-alipay-web/src/index.ts | 8 +- .../connector-apple/src/index.test.ts | 21 +- .../connectors/connector-apple/src/index.ts | 2 + .../connectors/connector-azuread/package.json | 13 +- .../connector-azuread/src/index.test.ts | 69 +- .../connectors/connector-azuread/src/index.ts | 12 +- .../connector-discord/src/index.test.ts | 9 +- .../connectors/connector-discord/src/index.ts | 6 +- .../connector-facebook/src/index.test.ts | 8 +- .../connector-facebook/src/index.ts | 5 +- .../connector-feishu-web/src/index.test.ts | 6 +- .../connector-feishu-web/src/index.ts | 2 + .../connector-github/src/index.test.ts | 16 +- .../connectors/connector-github/src/index.ts | 5 +- .../connector-google/src/index.test.ts | 6 +- .../connectors/connector-google/src/index.ts | 5 +- .../connector-kakao/src/index.test.ts | 28 +- .../connectors/connector-kakao/src/index.ts | 5 +- .../connector-mock-social/src/index.ts | 8 +- .../connector-naver/src/index.test.ts | 32 +- .../connectors/connector-naver/src/index.ts | 5 +- .../connector-oauth2/src/index.test.ts | 3 +- .../connectors/connector-oauth2/src/index.ts | 3 +- .../connector-oidc/src/index.test.ts | 2 +- .../connectors/connector-oidc/src/index.ts | 2 + .../connector-wechat-native/src/index.test.ts | 7 +- .../connector-wechat-native/src/index.ts | 6 +- .../connector-wechat-web/src/index.test.ts | 7 +- .../connector-wechat-web/src/index.ts | 6 +- .../connector-wecom/src/index.test.ts | 8 +- .../connectors/connector-wecom/src/index.ts | 21 +- packages/integration-tests/src/constants.ts | 4 +- .../src/tests/api/admin-user.test.ts | 14 + .../social-interaction/happy-path.test.ts | 23 +- .../src/foundations/jsonb-types/index.ts | 16 +- packages/toolkit/connector-kit/src/index.ts | 39 +- .../toolkit/connector-kit/src/types/social.ts | 33 +- pnpm-lock.yaml | 678 +++++++++++++++++- 47 files changed, 1070 insertions(+), 154 deletions(-) create mode 100644 .changeset/gentle-shoes-push.md create mode 100644 .changeset/new-tables-breathe.md create mode 100644 .changeset/olive-cycles-sip.md create mode 100644 .changeset/wild-hotels-drop.md diff --git a/.changeset/gentle-shoes-push.md b/.changeset/gentle-shoes-push.md new file mode 100644 index 000000000..b2009325b --- /dev/null +++ b/.changeset/gentle-shoes-push.md @@ -0,0 +1,8 @@ +--- +"@logto/connector-kit": major +--- + +update `SocialUserInfo` and `GetUserInfo` types + +- Added `rawData?: Json` to `SocialUserInfo` +- `GetUserInfo` now does not accept unknown keys in the return object, since the raw data is now stored in `SocialUserInfo` diff --git a/.changeset/new-tables-breathe.md b/.changeset/new-tables-breathe.md new file mode 100644 index 000000000..9743f1e0a --- /dev/null +++ b/.changeset/new-tables-breathe.md @@ -0,0 +1,7 @@ +--- +"@logto/connector-kit": major +--- + +guard results of `parseJson` and `parseJsonObject` + +Now `parseJson` and `parseJsonObject` are type safe. diff --git a/.changeset/olive-cycles-sip.md b/.changeset/olive-cycles-sip.md new file mode 100644 index 000000000..4876e9f52 --- /dev/null +++ b/.changeset/olive-cycles-sip.md @@ -0,0 +1,21 @@ +--- +"@logto/connector-alipay-native": minor +"@logto/connector-wechat-native": minor +"@logto/connector-mock-social": minor +"@logto/connector-alipay-web": minor +"@logto/connector-feishu-web": minor +"@logto/connector-wechat-web": minor +"@logto/connector-facebook": minor +"@logto/connector-azuread": minor +"@logto/connector-discord": minor +"@logto/connector-github": minor +"@logto/connector-google": minor +"@logto/connector-oauth": minor +"@logto/connector-apple": minor +"@logto/connector-kakao": minor +"@logto/connector-naver": minor +"@logto/connector-wecom": minor +"@logto/connector-oidc": minor +--- + +return and store social connector raw data diff --git a/.changeset/wild-hotels-drop.md b/.changeset/wild-hotels-drop.md new file mode 100644 index 000000000..1e03d5dce --- /dev/null +++ b/.changeset/wild-hotels-drop.md @@ -0,0 +1,5 @@ +--- +"@logto/connector-kit": minor +--- + +add `jsonGuard()` and `jsonObjectGuard()` diff --git a/.gitpod.yml b/.gitpod.yml index 0974dbdc8..31dcc8e32 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -12,7 +12,7 @@ tasks: cd packages/core pnpm build cd - - pnpm connectors:build + pnpm connectors build pnpm cli connector link command: | gp ports await 5432 diff --git a/package.json b/package.json index 4752a4934..d64d30c2c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "cli": "logto", "changeset": "changeset", "alteration": "logto db alt", - "connectors:build": "pnpm -r --filter \"./packages/connectors/connector-*\" build", + "connectors": "pnpm -r --filter \"./packages/connectors/connector-*\"", "//": "# `changeset version` won't run version lifecycle scripts, see https://github.com/changesets/changesets/issues/860", "ci:version": "changeset version && pnpm -r version", "ci:build": "pnpm -r build", diff --git a/packages/connectors/connector-alipay-native/src/index.test.ts b/packages/connectors/connector-alipay-native/src/index.test.ts index 4ec5f4c42..cee532872 100644 --- a/packages/connectors/connector-alipay-native/src/index.test.ts +++ b/packages/connectors/connector-alipay-native/src/index.test.ts @@ -156,10 +156,23 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' }, jest.fn()); + const { id, name, avatar, rawData } = await connector.getUserInfo( + { auth_code: 'code' }, + jest.fn() + ); expect(id).toEqual('2088000000000000'); expect(name).toEqual('PlayboyEric'); expect(avatar).toEqual('https://www.alipay.com/xxx.jpg'); + expect(rawData).toEqual({ + alipay_user_info_share_response: { + code: '10000', + msg: 'Success', + user_id: '2088000000000000', + nick_name: 'PlayboyEric', + avatar: 'https://www.alipay.com/xxx.jpg', + }, + sign: '', + }); }); it('should throw SocialAccessTokenInvalid with code 20001', async () => { diff --git a/packages/connectors/connector-alipay-native/src/index.ts b/packages/connectors/connector-alipay-native/src/index.ts index fa412d68f..dc7d216eb 100644 --- a/packages/connectors/connector-alipay-native/src/index.ts +++ b/packages/connectors/connector-alipay-native/src/index.ts @@ -131,8 +131,8 @@ const getUserInfo = searchParams: signedSearchParameters, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -148,7 +148,7 @@ const getUserInfo = throw new ConnectorError(ConnectorErrorCodes.InvalidResponse); } - return { id, avatar, name }; + return { id, avatar, name, rawData }; }; const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => { diff --git a/packages/connectors/connector-alipay-web/src/index.test.ts b/packages/connectors/connector-alipay-web/src/index.test.ts index 2e0ad24fa..6c7085488 100644 --- a/packages/connectors/connector-alipay-web/src/index.test.ts +++ b/packages/connectors/connector-alipay-web/src/index.test.ts @@ -142,10 +142,23 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - const { id, name, avatar } = await connector.getUserInfo({ auth_code: 'code' }, jest.fn()); + const { id, name, avatar, rawData } = await connector.getUserInfo( + { auth_code: 'code' }, + jest.fn() + ); expect(id).toEqual('2088000000000000'); expect(name).toEqual('PlayboyEric'); expect(avatar).toEqual('https://www.alipay.com/xxx.jpg'); + expect(rawData).toEqual({ + alipay_user_info_share_response: { + code: '10000', + msg: 'Success', + user_id: '2088000000000000', + nick_name: 'PlayboyEric', + avatar: 'https://www.alipay.com/xxx.jpg', + }, + sign: '', + }); }); it('throw General error if auth_code not provided in input', async () => { diff --git a/packages/connectors/connector-alipay-web/src/index.ts b/packages/connectors/connector-alipay-web/src/index.ts index 0329e5a4e..c3f592e44 100644 --- a/packages/connectors/connector-alipay-web/src/index.ts +++ b/packages/connectors/connector-alipay-web/src/index.ts @@ -130,10 +130,8 @@ const getUserInfo = searchParams: signedSearchParameters, timeout: { request: defaultTimeout }, }); - - const { body: rawBody } = httpResponse; - - const result = userInfoResponseGuard.safeParse(parseJson(rawBody)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -149,7 +147,7 @@ const getUserInfo = throw new ConnectorError(ConnectorErrorCodes.InvalidResponse); } - return { id, avatar, name }; + return { id, avatar, name, rawData }; }; const errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => { diff --git a/packages/connectors/connector-apple/src/index.test.ts b/packages/connectors/connector-apple/src/index.test.ts index fdf8c7771..9e6f41c39 100644 --- a/packages/connectors/connector-apple/src/index.test.ts +++ b/packages/connectors/connector-apple/src/index.test.ts @@ -60,7 +60,11 @@ describe('getUserInfo', () => { })); const connector = await createConnector({ getConfig }); const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, jest.fn()); - expect(userInfo).toEqual({ id: userId, email: 'foo@bar.com' }); + expect(userInfo).toEqual({ + id: userId, + email: 'foo@bar.com', + rawData: { id_token: 'idToken' }, + }); }); it('should ignore unverified email', async () => { @@ -69,7 +73,7 @@ describe('getUserInfo', () => { })); const connector = await createConnector({ getConfig }); const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, jest.fn()); - expect(userInfo).toEqual({ id: 'userId' }); + expect(userInfo).toEqual({ id: 'userId', rawData: { id_token: 'idToken' } }); }); it('should get user info from the `user` field', async () => { @@ -89,7 +93,18 @@ describe('getUserInfo', () => { jest.fn() ); // Should use info from `user` field first - expect(userInfo).toEqual({ id: userId, email: 'foo2@bar.com', name: 'foo bar' }); + expect(userInfo).toEqual({ + id: userId, + email: 'foo2@bar.com', + name: 'foo bar', + rawData: { + id_token: 'idToken', + user: JSON.stringify({ + email: 'foo2@bar.com', + name: { firstName: 'foo', lastName: 'bar' }, + }), + }, + }); }); it('should throw if id token is missing', async () => { diff --git a/packages/connectors/connector-apple/src/index.ts b/packages/connectors/connector-apple/src/index.ts index fa699aab4..bbd23d2d6 100644 --- a/packages/connectors/connector-apple/src/index.ts +++ b/packages/connectors/connector-apple/src/index.ts @@ -12,6 +12,7 @@ import { ConnectorErrorCodes, validateConfig, ConnectorType, + jsonGuard, } from '@logto/connector-kit'; import { generateStandardId } from '@logto/shared/universal'; import { createRemoteJWKSet, jwtVerify } from 'jose'; @@ -112,6 +113,7 @@ const getUserInfo = user?.email ?? (payload.email && payload.email_verified === true ? String(payload.email) : undefined), name: [user?.name?.firstName, user?.name?.lastName].filter(Boolean).join(' ') || undefined, + rawData: jsonGuard.parse(data), }; } catch { throw new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid); diff --git a/packages/connectors/connector-azuread/package.json b/packages/connectors/connector-azuread/package.json index fdb999b75..3f174bae6 100644 --- a/packages/connectors/connector-azuread/package.json +++ b/packages/connectors/connector-azuread/package.json @@ -4,8 +4,8 @@ "description": "Microsoft Azure AD connector implementation.", "author": "Mobilist Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", - "@azure/msal-node": "^2.0.0" + "@azure/msal-node": "^2.0.0", + "@logto/connector-kit": "workspace:^2.1.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -25,9 +25,8 @@ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "lint": "eslint --ext .ts src", "lint:report": "pnpm lint --format json --output-file report.json", - "test:only": "NODE_OPTIONS=--experimental-vm-modules jest", - "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm test:only --silent --coverage", + "test": "vitest src", + "test:ci": "pnpm run test --silent --coverage", "prepublishOnly": "pnpm build" }, "engines": { @@ -48,5 +47,9 @@ "prettier": "@silverhand/eslint-config/.prettierrc", "publishConfig": { "access": "public" + }, + "devDependencies": { + "@vitest/coverage-v8": "^1.4.0", + "vitest": "^1.4.0" } } diff --git a/packages/connectors/connector-azuread/src/index.test.ts b/packages/connectors/connector-azuread/src/index.test.ts index 0be0dd8f6..1368f85b6 100644 --- a/packages/connectors/connector-azuread/src/index.test.ts +++ b/packages/connectors/connector-azuread/src/index.test.ts @@ -1,13 +1,76 @@ -import type { GetConnectorConfig } from '@logto/connector-kit'; +import nock from 'nock'; +import { vi, describe, beforeAll, it, expect } from 'vitest'; + +import { graphAPIEndpoint } from './constant.js'; import createConnector from './index.js'; -const { jest } = import.meta; +vi.mock('@azure/msal-node', async () => ({ + ConfidentialClientApplication: class { + async acquireTokenByCode() { + return { + accessToken: 'accessToken', + scopes: ['scopes'], + tokenType: 'tokenType', + }; + } + }, +})); -const getConnectorConfig = jest.fn() as GetConnectorConfig; +const getConnectorConfig = vi.fn().mockResolvedValue({ + clientId: 'clientId', + clientSecret: 'clientSecret', + cloudInstance: 'https://login.microsoftonline.com', + tenantId: 'tenantId', +}); describe('Azure AD connector', () => { it('init without exploding', () => { expect(async () => createConnector({ getConfig: getConnectorConfig })).not.toThrow(); }); }); + +describe('getUserInfo', () => { + beforeAll(async () => { + const graphMeUrl = new URL(graphAPIEndpoint); + nock(graphMeUrl.origin).get(graphMeUrl.pathname).reply(200, { + id: 'id', + displayName: 'displayName', + mail: 'mail', + userPrincipalName: 'userPrincipalName', + }); + }); + + it('should get user info from graph api', async () => { + const connector = await createConnector({ getConfig: getConnectorConfig }); + const userInfo = await connector.getUserInfo( + { code: 'code', redirectUri: 'redirectUri' }, + vi.fn() + ); + expect(userInfo).toEqual({ + id: 'id', + name: 'displayName', + email: 'mail', + rawData: { + id: 'id', + displayName: 'displayName', + mail: 'mail', + userPrincipalName: 'userPrincipalName', + }, + }); + }); + + it('should throw if graph api response has no id', async () => { + const graphMeUrl = new URL(graphAPIEndpoint); + nock(graphMeUrl.origin).get(graphMeUrl.pathname).reply(200, { + displayName: 'displayName', + mail: 'mail', + userPrincipalName: 'userPrincipalName', + }); + + const connector = await createConnector({ getConfig: getConnectorConfig }); + const userInfo = connector.getUserInfo({ code: 'code', redirectUri: 'redirectUri' }, vi.fn()); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await expect(userInfo).rejects.toThrow(expect.objectContaining({ code: 'invalid_response' })); + }); +}); diff --git a/packages/connectors/connector-azuread/src/index.ts b/packages/connectors/connector-azuread/src/index.ts index 91c95332d..eec02121a 100644 --- a/packages/connectors/connector-azuread/src/index.ts +++ b/packages/connectors/connector-azuread/src/index.ts @@ -3,7 +3,7 @@ import { got, HTTPError } from 'got'; import path from 'node:path'; import type { AuthorizationCodeRequest, AuthorizationUrlRequest } from '@azure/msal-node'; -import { ConfidentialClientApplication, CryptoProvider } from '@azure/msal-node'; +import { ConfidentialClientApplication } from '@azure/msal-node'; import type { GetAuthorizationUri, GetUserInfo, @@ -31,10 +31,6 @@ import { // eslint-disable-next-line @silverhand/fp/no-let let authCodeRequest: AuthorizationCodeRequest; -// This `cryptoProvider` seems not used. -// Temporarily keep this as this is a refactor, which should not change the logics. -const cryptoProvider = new CryptoProvider(); - const getAuthorizationUri = (getConfig: GetConnectorConfig): GetAuthorizationUri => async ({ state, redirectUri }) => { @@ -85,7 +81,6 @@ const getAccessToken = async (config: AzureADConfig, code: string, redirectUri: }); const authResult = await clientApplication.acquireTokenByCode(codeRequest); - const result = accessTokenResponseGuard.safeParse(authResult); if (!result.success) { @@ -117,8 +112,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -130,6 +125,7 @@ const getUserInfo = id, email: conditional(mail), name: conditional(displayName), + rawData, }; } catch (error: unknown) { if (error instanceof HTTPError) { diff --git a/packages/connectors/connector-discord/src/index.test.ts b/packages/connectors/connector-discord/src/index.test.ts index 31a20641a..333d0ff29 100644 --- a/packages/connectors/connector-discord/src/index.test.ts +++ b/packages/connectors/connector-discord/src/index.test.ts @@ -101,11 +101,18 @@ describe('Discord connector', () => { }, jest.fn() ); - expect(socialUserInfo).toMatchObject({ + expect(socialUserInfo).toStrictEqual({ id: '1234567890', name: 'Whumpus', avatar: 'https://cdn.discordapp.com/avatars/1234567890/avatar_id', email: 'whumpus@discord.com', + rawData: { + id: '1234567890', + username: 'Whumpus', + avatar: 'avatar_id', + email: 'whumpus@discord.com', + verified: true, + }, }); }); diff --git a/packages/connectors/connector-discord/src/index.ts b/packages/connectors/connector-discord/src/index.ts index 224543952..4483cc2e9 100644 --- a/packages/connectors/connector-discord/src/index.ts +++ b/packages/connectors/connector-discord/src/index.ts @@ -102,8 +102,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -124,7 +124,7 @@ const getUserInfo = throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, userInfoResult.error); } - return userInfoResult.data; + return { ...userInfoResult.data, rawData }; } catch (error: unknown) { if (error instanceof HTTPError) { const { statusCode, body: rawBody } = error.response; diff --git a/packages/connectors/connector-facebook/src/index.test.ts b/packages/connectors/connector-facebook/src/index.test.ts index d09193289..979fdf378 100644 --- a/packages/connectors/connector-facebook/src/index.test.ts +++ b/packages/connectors/connector-facebook/src/index.test.ts @@ -132,11 +132,17 @@ describe('Facebook connector', () => { }, jest.fn() ); - expect(socialUserInfo).toMatchObject({ + expect(socialUserInfo).toStrictEqual({ id: '1234567890', avatar, name: 'monalisa octocat', email: 'octocat@facebook.com', + rawData: { + id: '1234567890', + name: 'monalisa octocat', + email: 'octocat@facebook.com', + picture: { data: { url: avatar } }, + }, }); }); diff --git a/packages/connectors/connector-facebook/src/index.ts b/packages/connectors/connector-facebook/src/index.ts index 7cb622c0f..0aa677a24 100644 --- a/packages/connectors/connector-facebook/src/index.ts +++ b/packages/connectors/connector-facebook/src/index.ts @@ -105,8 +105,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -119,6 +119,7 @@ const getUserInfo = avatar: picture?.data.url, email, name, + rawData, }; } catch (error: unknown) { if (error instanceof HTTPError) { diff --git a/packages/connectors/connector-feishu-web/src/index.test.ts b/packages/connectors/connector-feishu-web/src/index.test.ts index dc9871c97..118d31f9b 100644 --- a/packages/connectors/connector-feishu-web/src/index.test.ts +++ b/packages/connectors/connector-feishu-web/src/index.test.ts @@ -112,7 +112,7 @@ describe('getUserInfo', () => { const accessTokenUrl = new URL(accessTokenEndpoint); it('should get userInfo with accessToken', async () => { - nock(userInfoUrl.origin).get(userInfoUrl.pathname).query(true).once().reply(200, { + const jsonResponse = Object.freeze({ sub: 'ou_caecc734c2e3328a62489fe0648c4b98779515d3', name: '李雷', picture: 'https://www.feishu.cn/avatar', @@ -129,9 +129,10 @@ describe('getUserInfo', () => { employee_no: '111222333', mobile: '+86130xxxx0000', }); + nock(userInfoUrl.origin).get(userInfoUrl.pathname).query(true).once().reply(200, jsonResponse); const connector = await createConnector({ getConfig }); - const { id, name, avatar } = await connector.getUserInfo( + const { id, name, avatar, rawData } = await connector.getUserInfo( { code: 'code', redirectUri: 'http://localhost:3000', @@ -141,6 +142,7 @@ describe('getUserInfo', () => { expect(id).toEqual('ou_caecc734c2e3328a62489fe0648c4b98779515d3'); expect(name).toEqual('李雷'); expect(avatar).toEqual('www.feishu.cn/avatar/icon'); + expect(rawData).toEqual(jsonResponse); }); it('throw General error if code not provided in input', async () => { diff --git a/packages/connectors/connector-feishu-web/src/index.ts b/packages/connectors/connector-feishu-web/src/index.ts index d5c8f36f7..a2b25b2f9 100644 --- a/packages/connectors/connector-feishu-web/src/index.ts +++ b/packages/connectors/connector-feishu-web/src/index.ts @@ -13,6 +13,7 @@ import { ConnectorErrorCodes, ConnectorPlatform, ConnectorType, + jsonGuard, validateConfig, } from '@logto/connector-kit'; @@ -153,6 +154,7 @@ export function getUserInfo(getConfig: GetConnectorConfig): GetUserInfo { email: conditional(email), userId: conditional(user_id), phone: conditional(mobile), + rawData: jsonGuard.parse(response.body), }; } catch (error: unknown) { if (error instanceof ConnectorError) { diff --git a/packages/connectors/connector-github/src/index.test.ts b/packages/connectors/connector-github/src/index.test.ts index 6a5df4cf5..24c2b67dd 100644 --- a/packages/connectors/connector-github/src/index.test.ts +++ b/packages/connectors/connector-github/src/index.test.ts @@ -91,14 +91,22 @@ describe('getUserInfo', () => { avatar_url: 'https://github.com/images/error/octocat_happy.gif', name: 'monalisa octocat', email: 'octocat@github.com', + foo: 'bar', }); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo({ code: 'code' }, jest.fn()); - expect(socialUserInfo).toMatchObject({ + expect(socialUserInfo).toStrictEqual({ id: '1', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'monalisa octocat', email: 'octocat@github.com', + rawData: { + id: 1, + avatar_url: 'https://github.com/images/error/octocat_happy.gif', + name: 'monalisa octocat', + email: 'octocat@github.com', + foo: 'bar', + }, }); }); @@ -113,6 +121,12 @@ describe('getUserInfo', () => { const socialUserInfo = await connector.getUserInfo({ code: 'code' }, jest.fn()); expect(socialUserInfo).toMatchObject({ id: '1', + rawData: { + id: 1, + avatar_url: null, + name: null, + email: null, + }, }); }); diff --git a/packages/connectors/connector-github/src/index.ts b/packages/connectors/connector-github/src/index.ts index 547ce3964..52a0e97f1 100644 --- a/packages/connectors/connector-github/src/index.ts +++ b/packages/connectors/connector-github/src/index.ts @@ -117,8 +117,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -131,6 +131,7 @@ const getUserInfo = avatar: conditional(avatar), email: conditional(email), name: conditional(name), + rawData, }; } catch (error: unknown) { if (error instanceof HTTPError) { diff --git a/packages/connectors/connector-google/src/index.test.ts b/packages/connectors/connector-google/src/index.test.ts index 21560f0ad..32d2ed09f 100644 --- a/packages/connectors/connector-google/src/index.test.ts +++ b/packages/connectors/connector-google/src/index.test.ts @@ -79,7 +79,7 @@ describe('google connector', () => { }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpoint).post('').reply(200, { + const jsonResponse = Object.freeze({ sub: '1234567890', name: 'monalisa octocat', given_name: 'monalisa', @@ -89,6 +89,7 @@ describe('google connector', () => { email_verified: true, locale: 'en', }); + nock(userInfoEndpoint).post('').reply(200, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { @@ -97,11 +98,12 @@ describe('google connector', () => { }, jest.fn() ); - expect(socialUserInfo).toMatchObject({ + expect(socialUserInfo).toStrictEqual({ id: '1234567890', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'monalisa octocat', email: 'octocat@google.com', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-google/src/index.ts b/packages/connectors/connector-google/src/index.ts index db6a6ad09..19c8fd9a2 100644 --- a/packages/connectors/connector-google/src/index.ts +++ b/packages/connectors/connector-google/src/index.ts @@ -101,8 +101,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -115,6 +115,7 @@ const getUserInfo = avatar, email: conditional(email_verified && email), name, + rawData, }; } catch (error: unknown) { return getUserInfoErrorHandler(error); diff --git a/packages/connectors/connector-kakao/src/index.test.ts b/packages/connectors/connector-kakao/src/index.test.ts index 90db9c0cd..682e0e0a8 100644 --- a/packages/connectors/connector-kakao/src/index.test.ts +++ b/packages/connectors/connector-kakao/src/index.test.ts @@ -79,20 +79,19 @@ describe('kakao connector', () => { }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpoint) - .post('') - .reply(200, { - id: 1_234_567_890, - kakao_account: { - is_email_valid: true, - email: 'ruddbs5302@gmail.com', - profile: { - nickname: 'pemassi', - profile_image_url: 'https://github.com/images/error/octocat_happy.gif', - is_default_image: false, - }, + const jsonResponse = Object.freeze({ + id: 1_234_567_890, + kakao_account: { + is_email_valid: true, + email: 'ruddbs5302@gmail.com', + profile: { + nickname: 'pemassi', + profile_image_url: 'https://github.com/images/error/octocat_happy.gif', + is_default_image: false, }, - }); + }, + }); + nock(userInfoEndpoint).post('').reply(200, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { @@ -101,11 +100,12 @@ describe('kakao connector', () => { }, jest.fn() ); - expect(socialUserInfo).toMatchObject({ + expect(socialUserInfo).toStrictEqual({ id: '1234567890', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'pemassi', email: 'ruddbs5302@gmail.com', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-kakao/src/index.ts b/packages/connectors/connector-kakao/src/index.ts index 1c8dbf726..a07eadb8d 100644 --- a/packages/connectors/connector-kakao/src/index.ts +++ b/packages/connectors/connector-kakao/src/index.ts @@ -99,8 +99,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -118,6 +118,7 @@ const getUserInfo = avatar: conditional(profile && !profile.is_default_image && profile.profile_image_url), email: conditional(is_email_valid && email), name: conditional(profile?.nickname), + rawData, }; } catch (error: unknown) { return getUserInfoErrorHandler(error); diff --git a/packages/connectors/connector-mock-social/src/index.ts b/packages/connectors/connector-mock-social/src/index.ts index 2b6d0d466..b9c76058c 100644 --- a/packages/connectors/connector-mock-social/src/index.ts +++ b/packages/connectors/connector-mock-social/src/index.ts @@ -7,7 +7,12 @@ import type { CreateConnector, SocialConnector, } from '@logto/connector-kit'; -import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; +import { + ConnectorError, + ConnectorErrorCodes, + ConnectorType, + jsonGuard, +} from '@logto/connector-kit'; import { defaultMetadata } from './constant.js'; import { mockSocialConfigGuard } from './types.js'; @@ -35,6 +40,7 @@ const getUserInfo: GetUserInfo = async (data) => { return { id: userId ?? `mock-social-sub-${randomUUID()}`, ...rest, + rawData: jsonGuard.parse(data), }; }; diff --git a/packages/connectors/connector-naver/src/index.test.ts b/packages/connectors/connector-naver/src/index.test.ts index e41914637..18b8c656a 100644 --- a/packages/connectors/connector-naver/src/index.test.ts +++ b/packages/connectors/connector-naver/src/index.test.ts @@ -79,22 +79,21 @@ describe('naver connector', () => { }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpoint) - .post('') - .reply(200, { - resultcode: '00', - message: 'success', - response: { - email: 'openapi@naver.com', - nickname: 'OpenAPI', - profile_image: 'https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif', - age: '40-49', - gender: 'F', - id: '32742776', - name: '오픈 API', - birthday: '10-01', - }, - }); + const jsonResponse = Object.freeze({ + resultcode: '00', + message: 'success', + response: { + email: 'openapi@naver.com', + nickname: 'OpenAPI', + profile_image: 'https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif', + age: '40-49', + gender: 'F', + id: '32742776', + name: '오픈 API', + birthday: '10-01', + }, + }); + nock(userInfoEndpoint).post('').reply(200, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { @@ -108,6 +107,7 @@ describe('naver connector', () => { avatar: 'https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif', name: 'OpenAPI', email: 'openapi@naver.com', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-naver/src/index.ts b/packages/connectors/connector-naver/src/index.ts index 0a85a9c31..3e553e4bd 100644 --- a/packages/connectors/connector-naver/src/index.ts +++ b/packages/connectors/connector-naver/src/index.ts @@ -99,8 +99,8 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -114,6 +114,7 @@ const getUserInfo = avatar: conditional(profile_image), email: conditional(email), name: conditional(nickname), + rawData, }; } catch (error: unknown) { return getUserInfoErrorHandler(error); diff --git a/packages/connectors/connector-oauth2/src/index.test.ts b/packages/connectors/connector-oauth2/src/index.test.ts index d99681876..d490cbe29 100644 --- a/packages/connectors/connector-oauth2/src/index.test.ts +++ b/packages/connectors/connector-oauth2/src/index.test.ts @@ -59,6 +59,7 @@ describe('getUserInfo', () => { const userInfoEndpointUrl = new URL(mockConfig.userInfoEndpoint); nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(true).reply(200, { sub: userId, + foo: 'bar', }); const connector = await createConnector({ getConfig }); const userInfo = await connector.getUserInfo( @@ -67,6 +68,6 @@ describe('getUserInfo', () => { return { redirectUri: 'http://localhost:3001/callback' }; }) ); - expect(userInfo).toEqual({ id: userId }); + expect(userInfo).toStrictEqual({ id: userId, rawData: { sub: userId, foo: 'bar' } }); }); }); diff --git a/packages/connectors/connector-oauth2/src/index.ts b/packages/connectors/connector-oauth2/src/index.ts index 4d952dc58..f2b1c274b 100644 --- a/packages/connectors/connector-oauth2/src/index.ts +++ b/packages/connectors/connector-oauth2/src/index.ts @@ -71,8 +71,9 @@ const getUserInfo = }, timeout: { request: defaultTimeout }, }); + const rawData = parseJsonObject(httpResponse.body); - return userProfileMapping(parseJsonObject(httpResponse.body), parsedConfig.profileMap); + return { ...userProfileMapping(rawData, parsedConfig.profileMap), rawData }; } catch (error: unknown) { if (error instanceof HTTPError) { throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(error.response.body)); diff --git a/packages/connectors/connector-oidc/src/index.test.ts b/packages/connectors/connector-oidc/src/index.test.ts index be4033678..d064f08bf 100644 --- a/packages/connectors/connector-oidc/src/index.test.ts +++ b/packages/connectors/connector-oidc/src/index.test.ts @@ -69,6 +69,6 @@ describe('getUserInfo', () => { return { nonce: 'nonce', redirectUri: 'http://localhost:3001/callback' }; }) ); - expect(userInfo).toEqual({ id: userId }); + expect(userInfo).toMatchObject({ id: userId, rawData: { sub: userId, nonce: 'nonce' } }); }); }); diff --git a/packages/connectors/connector-oidc/src/index.ts b/packages/connectors/connector-oidc/src/index.ts index ae8e6d749..a413a26c0 100644 --- a/packages/connectors/connector-oidc/src/index.ts +++ b/packages/connectors/connector-oidc/src/index.ts @@ -14,6 +14,7 @@ import { ConnectorErrorCodes, validateConfig, ConnectorType, + jsonGuard, } from '@logto/connector-kit'; import { generateStandardId } from '@logto/shared/universal'; import { createRemoteJWKSet, jwtVerify } from 'jose'; @@ -137,6 +138,7 @@ const getUserInfo = avatar: conditional(picture), email: conditional(email_verified && email), phone: conditional(phone_verified && phone), + rawData: jsonGuard.parse(payload), }; } catch (error: unknown) { if (error instanceof HTTPError) { diff --git a/packages/connectors/connector-wechat-native/src/index.test.ts b/packages/connectors/connector-wechat-native/src/index.test.ts index 15a5f449e..94dfcbdaa 100644 --- a/packages/connectors/connector-wechat-native/src/index.test.ts +++ b/packages/connectors/connector-wechat-native/src/index.test.ts @@ -139,17 +139,22 @@ describe('getUserInfo', () => { 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, { + const jsonResponse = Object.freeze({ unionid: 'this_is_an_arbitrary_wechat_union_id', headimgurl: 'https://github.com/images/error/octocat_happy.gif', nickname: 'wechat bot', }); + nock(userInfoEndpointUrl.origin) + .get(userInfoEndpointUrl.pathname) + .query(parameters) + .reply(0, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo({ code: 'code' }, jest.fn()); expect(socialUserInfo).toMatchObject({ id: 'this_is_an_arbitrary_wechat_union_id', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'wechat bot', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-wechat-native/src/index.ts b/packages/connectors/connector-wechat-native/src/index.ts index fc976cc09..e2fe4f9ec 100644 --- a/packages/connectors/connector-wechat-native/src/index.ts +++ b/packages/connectors/connector-wechat-native/src/index.ts @@ -100,8 +100,8 @@ const getUserInfo = searchParams: { access_token: accessToken, openid }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -114,7 +114,7 @@ const getUserInfo = // 'errmsg' and 'errcode' turn to non-empty values or empty values at the same time. Hence, if 'errmsg' is non-empty then 'errcode' should be non-empty. userInfoResponseMessageParser(result.data); - return { id: unionid ?? openid, avatar: headimgurl, name: nickname }; + return { id: unionid ?? openid, avatar: headimgurl, name: nickname, rawData }; } catch (error: unknown) { return getUserInfoErrorHandler(error); } diff --git a/packages/connectors/connector-wechat-web/src/index.test.ts b/packages/connectors/connector-wechat-web/src/index.test.ts index 6d03e72b0..f43a84a6f 100644 --- a/packages/connectors/connector-wechat-web/src/index.test.ts +++ b/packages/connectors/connector-wechat-web/src/index.test.ts @@ -130,11 +130,15 @@ describe('getUserInfo', () => { 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, { + const jsonResponse = Object.freeze({ unionid: 'this_is_an_arbitrary_wechat_union_id', headimgurl: 'https://github.com/images/error/octocat_happy.gif', nickname: 'wechat bot', }); + nock(userInfoEndpointUrl.origin) + .get(userInfoEndpointUrl.pathname) + .query(parameters) + .reply(0, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { @@ -146,6 +150,7 @@ describe('getUserInfo', () => { id: 'this_is_an_arbitrary_wechat_union_id', avatar: 'https://github.com/images/error/octocat_happy.gif', name: 'wechat bot', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-wechat-web/src/index.ts b/packages/connectors/connector-wechat-web/src/index.ts index 7e1d96ff4..495414572 100644 --- a/packages/connectors/connector-wechat-web/src/index.ts +++ b/packages/connectors/connector-wechat-web/src/index.ts @@ -101,8 +101,8 @@ const getUserInfo = searchParams: { access_token: accessToken, openid }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -115,7 +115,7 @@ const getUserInfo = // 'errmsg' and 'errcode' turn to non-empty values or empty values at the same time. Hence, if 'errmsg' is non-empty then 'errcode' should be non-empty. userInfoResponseMessageParser(result.data); - return { id: unionid ?? openid, avatar: headimgurl, name: nickname }; + return { id: unionid ?? openid, avatar: headimgurl, name: nickname, rawData }; } catch (error: unknown) { return getUserInfoErrorHandler(error); } diff --git a/packages/connectors/connector-wecom/src/index.test.ts b/packages/connectors/connector-wecom/src/index.test.ts index 474b1949d..ac05c9610 100644 --- a/packages/connectors/connector-wecom/src/index.test.ts +++ b/packages/connectors/connector-wecom/src/index.test.ts @@ -135,9 +135,14 @@ describe('getUserInfo', () => { const parameters = new URLSearchParams({ access_token: 'access_token', code: 'code' }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(0, { + const jsonResponse = Object.freeze({ userid: 'wecom_id', + foo: 'bar', }); + nock(userInfoEndpointUrl.origin) + .get(userInfoEndpointUrl.pathname) + .query(parameters) + .reply(0, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { @@ -149,6 +154,7 @@ describe('getUserInfo', () => { id: 'wecom_id', avatar: '', name: 'wecom_id', + rawData: jsonResponse, }); }); diff --git a/packages/connectors/connector-wecom/src/index.ts b/packages/connectors/connector-wecom/src/index.ts index 1f8ee1114..266f2be84 100644 --- a/packages/connectors/connector-wecom/src/index.ts +++ b/packages/connectors/connector-wecom/src/index.ts @@ -106,8 +106,8 @@ const getUserInfo = searchParams: { access_token: accessToken, code }, timeout: { request: defaultTimeout }, }); - - const result = userInfoResponseGuard.safeParse(parseJson(httpResponse.body)); + const rawData = parseJson(httpResponse.body); + const result = userInfoResponseGuard.safeParse(rawData); if (!result.success) { throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error); @@ -118,15 +118,18 @@ const getUserInfo = errorResponseHandler(result.data); // const { userid, openid } = result.data; + const id = userid ?? openid; - if (userid) { - return { id: userid, avatar: '', name: userid }; + if (!id) { + throw new Error('Both userid and openid are undefined or null.'); } - if (openid) { - return { id: openid, avatar: '', name: openid }; - } - throw new Error('Both userid and openid are undefined or null.'); - // Both userid and openid are null + + return { + id, + avatar: '', + name: id, + rawData, + }; } catch (error: unknown) { return getUserInfoErrorHandler(error); } diff --git a/packages/integration-tests/src/constants.ts b/packages/integration-tests/src/constants.ts index f2d140d8c..a947f4548 100644 --- a/packages/integration-tests/src/constants.ts +++ b/packages/integration-tests/src/constants.ts @@ -1,8 +1,8 @@ import { type CreateSsoConnector, SignInIdentifier, - demoAppApplicationId, SsoProviderName, + demoAppApplicationId, } from '@logto/schemas'; import { appendPath, getEnv } from '@silverhand/essentials'; @@ -17,7 +17,7 @@ export const demoAppUrl = appendPath(new URL(logtoUrl), 'demo-app'); export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`; -export const demoAppRedirectUri = `${logtoUrl}/${demoAppApplicationId}`; +export const demoAppRedirectUri = appendPath(new URL(logtoUrl), demoAppApplicationId).href; export const adminConsoleRedirectUri = `${logtoConsoleUrl}/console/callback`; export const signUpIdentifiers = { diff --git a/packages/integration-tests/src/tests/api/admin-user.test.ts b/packages/integration-tests/src/tests/api/admin-user.test.ts index 72fd002f7..b12bbf955 100644 --- a/packages/integration-tests/src/tests/api/admin-user.test.ts +++ b/packages/integration-tests/src/tests/api/admin-user.test.ts @@ -193,6 +193,13 @@ describe('admin console user management', () => { details: { id: socialUserId, email: socialUserEmail, + rawData: { + code, + email: socialUserEmail, + redirectUri, + state, + userId: socialUserId, + }, }, }); @@ -202,6 +209,13 @@ describe('admin console user management', () => { expect(updatedIdentity).toHaveProperty(mockSocialConnectorTarget); expect(updatedIdentity[mockSocialConnectorTarget]).toMatchObject({ userId: anotherSocialUserId, + rawData: { + code, + email: socialUserEmail, + redirectUri, + state, + userId: anotherSocialUserId, + }, }); const updatedIdentities = await putUserIdentity(userId, socialTarget, socialIdentity); diff --git a/packages/integration-tests/src/tests/api/interaction/social-interaction/happy-path.test.ts b/packages/integration-tests/src/tests/api/interaction/social-interaction/happy-path.test.ts index 8fe614651..961639228 100644 --- a/packages/integration-tests/src/tests/api/interaction/social-interaction/happy-path.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/social-interaction/happy-path.test.ts @@ -1,6 +1,9 @@ import { ConnectorType, InteractionEvent, SignInIdentifier } from '@logto/schemas'; -import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js'; +import { + mockSocialConnectorId, + mockSocialConnectorTarget, +} from '#src/__mocks__/connectors-mock.js'; import { createSocialAuthorizationUri, putInteraction, @@ -131,8 +134,24 @@ describe('Social Identifier Interactions', () => { const uid = await processSession(client, redirectTo); - const { primaryEmail } = await getUser(uid); + const { primaryEmail, identities } = await getUser(uid); expect(primaryEmail).toBe(socialEmail); + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + expect(identities[mockSocialConnectorTarget]).toStrictEqual({ + details: { + email: expect.any(String), + id: expect.any(String), + rawData: { + code: 'auth_code_foo', + email: expect.any(String), + redirectUri: 'http://foo.dev/callback', + state: 'foo_state', + userId: expect.any(String), + }, + }, + userId: expect.any(String), + }); + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ await logoutClient(client); await deleteUser(uid); diff --git a/packages/schemas/src/foundations/jsonb-types/index.ts b/packages/schemas/src/foundations/jsonb-types/index.ts index f7eda8966..601855d1f 100644 --- a/packages/schemas/src/foundations/jsonb-types/index.ts +++ b/packages/schemas/src/foundations/jsonb-types/index.ts @@ -1,6 +1,3 @@ -import type { Json } from '@withtyped/server'; -import { z } from 'zod'; - export * from './custom-domain.js'; export * from './hooks.js'; export * from './logs.js'; @@ -15,17 +12,8 @@ export * from './applications.js'; export { configurableConnectorMetadataGuard, type ConfigurableConnectorMetadata, + jsonGuard, + jsonObjectGuard, } from '@logto/connector-kit'; export type { Json, JsonObject } from '@withtyped/server'; - -/* === Commonly Used === */ - -// Copied from https://github.com/colinhacks/zod#json-type -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); - -export const jsonGuard: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(jsonGuard), z.record(jsonGuard)]) -); - -export const jsonObjectGuard = z.record(jsonGuard); diff --git a/packages/toolkit/connector-kit/src/index.ts b/packages/toolkit/connector-kit/src/index.ts index 68b33e00e..b9f560fe1 100644 --- a/packages/toolkit/connector-kit/src/index.ts +++ b/packages/toolkit/connector-kit/src/index.ts @@ -1,3 +1,4 @@ +import { type Json, type JsonObject } from '@withtyped/server'; import type { ZodType, ZodTypeDef } from 'zod'; import { @@ -5,6 +6,8 @@ import { ConnectorErrorCodes, type SendMessagePayload, ConnectorType, + jsonGuard, + jsonObjectGuard, } from './types/index.js'; export * from './types/index.js'; @@ -24,38 +27,24 @@ export const parseJson = ( jsonString: string, errorCode: ConnectorErrorCodes = ConnectorErrorCodes.InvalidResponse, errorPayload?: unknown -): unknown => { +): Json => { try { - return JSON.parse(jsonString); + return jsonGuard.parse(JSON.parse(jsonString)); } catch { throw new ConnectorError(errorCode, errorPayload ?? jsonString); } }; -const isRecordOrArray = (parsed: unknown): parsed is Record | unknown[] => { - if (Array.isArray(parsed)) { - return true; +export const parseJsonObject = ( + ...[jsonString, errorCode = ConnectorErrorCodes.InvalidResponse, errorPayload]: Parameters< + typeof parseJson + > +): JsonObject => { + try { + return jsonObjectGuard.parse(JSON.parse(jsonString)); + } catch { + throw new ConnectorError(errorCode, errorPayload ?? jsonString); } - - if (!(parsed !== null && typeof parsed === 'object')) { - return false; - } - - if (Object.getOwnPropertySymbols(parsed).length > 0) { - return false; - } - - return true; -}; - -export const parseJsonObject = (...args: Parameters) => { - const parsed = parseJson(...args); - - if (!isRecordOrArray(parsed)) { - throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsed); - } - - return parsed; }; /** @deprecated Use {@link mockConnectorFilePaths} instead. */ diff --git a/packages/toolkit/connector-kit/src/types/social.ts b/packages/toolkit/connector-kit/src/types/social.ts index 865eb0318..71f2008e6 100644 --- a/packages/toolkit/connector-kit/src/types/social.ts +++ b/packages/toolkit/connector-kit/src/types/social.ts @@ -1,4 +1,5 @@ // MARK: Social connector +import { type Json } from '@withtyped/server'; import { z } from 'zod'; import { type BaseConnector, type ConnectorType } from './foundation.js'; @@ -22,20 +23,38 @@ export type GetAuthorizationUri = ( setSession: SetSession ) => Promise; +// Copied from https://github.com/colinhacks/zod#json-type +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); + +export const jsonGuard: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonGuard), z.record(jsonGuard)]) +); + +export const jsonObjectGuard = z.record(jsonGuard); + +/** + * Normalized social user info that can be used in the system. The raw data returned from the + * social provider is also included in the `rawData` field. + */ +export type SocialUserInfo = { + id: string; + email?: string; + phone?: string; + name?: string; + avatar?: string; + rawData?: Json; +}; + export const socialUserInfoGuard = z.object({ id: z.string(), email: z.string().optional(), phone: z.string().optional(), name: z.string().optional(), avatar: z.string().optional(), -}); + rawData: jsonGuard.optional(), +}) satisfies z.ZodType; -export type SocialUserInfo = z.infer; - -export type GetUserInfo = ( - data: unknown, - getSession: GetSession -) => Promise>; +export type GetUserInfo = (data: unknown, getSession: GetSession) => Promise; export const connectorSessionGuard = z .object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c08d806d7..c14321398 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -786,6 +786,9 @@ importers: '@types/supertest': specifier: ^6.0.0 version: 6.0.1 + '@vitest/coverage-v8': + specifier: ^1.4.0 + version: 1.4.0(vitest@1.4.0) eslint: specifier: ^8.44.0 version: 8.44.0 @@ -816,6 +819,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.3.3 + vitest: + specifier: ^1.4.0 + version: 1.4.0(@types/node@20.11.20) packages/connectors/connector-discord: dependencies: @@ -4274,6 +4280,14 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: true + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@apidevtools/json-schema-ref-parser@9.0.6: resolution: {integrity: sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==} dependencies: @@ -6264,6 +6278,16 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} @@ -6302,6 +6326,14 @@ packages: '@babel/types': 7.20.2 dev: true + /@babel/parser@7.24.0: + resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + dev: true + /@babel/plugin-proposal-object-rest-spread@7.12.1(@babel/core@7.12.9): resolution: {integrity: sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==} peerDependencies: @@ -6525,6 +6557,15 @@ packages: to-fast-properties: 2.0.0 dev: true + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true @@ -7007,6 +7048,213 @@ packages: postcss-selector-parser: 6.0.15 dev: true + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.44.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7510,6 +7758,15 @@ packages: '@jridgewell/trace-mapping': 0.3.18 dev: true + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -7520,6 +7777,11 @@ packages: engines: {node: '>=6.0.0'} dev: true + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + /@jridgewell/source-map@0.3.5: resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} dependencies: @@ -7542,6 +7804,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -10573,6 +10842,69 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true + /@vitest/coverage-v8@1.4.0(vitest@1.4.0): + resolution: {integrity: sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==} + peerDependencies: + vitest: 1.4.0 + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.7 + magic-string: 0.30.7 + magicast: 0.3.3 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + test-exclude: 6.0.0 + v8-to-istanbul: 9.2.0 + vitest: 1.4.0(@types/node@20.11.20) + transitivePeerDependencies: + - supports-color + dev: true + + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + dependencies: + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 + dev: true + + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + dependencies: + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + dependencies: + magic-string: 0.30.7 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + dependencies: + tinyspy: 2.2.1 + dev: true + + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@withtyped/client@0.8.4(zod@3.22.4): resolution: {integrity: sha512-oX19xZRjUASZLWUzQW4LKhLsDgtwFfWU7mgReKiY/tddwvHLoakLqz1o1MvRDuz+EOoYrQM+VpkwJeMsnbNT1w==} dependencies: @@ -10659,12 +10991,23 @@ packages: engines: {node: '>=0.4.0'} dev: true + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -10907,6 +11250,10 @@ packages: tslib: 2.6.2 dev: false + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + /ast-types-flow@0.0.7: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true @@ -11233,6 +11580,11 @@ packages: engines: {node: '>= 0.8'} dev: false + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + /cache-content-type@1.0.1: resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} engines: {node: '>= 6.0.0'} @@ -11350,6 +11702,19 @@ packages: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} dev: true + /chai@4.4.1: + resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.3 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -11398,6 +11763,12 @@ packages: /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 + dev: true + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -12163,6 +12534,13 @@ packages: optional: true dev: true + /deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + /deep-equal@1.0.1: resolution: {integrity: sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=} @@ -12574,6 +12952,37 @@ packages: is-symbol: 1.0.4 dev: true + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -13104,6 +13513,12 @@ packages: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -13680,6 +14095,10 @@ packages: engines: {node: '>=18'} dev: false + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true + /get-intrinsic@1.1.3: resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} dependencies: @@ -14893,6 +15312,11 @@ packages: engines: {node: '>=8'} dev: true + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + /istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} engines: {node: '>=8'} @@ -14928,6 +15352,15 @@ packages: supports-color: 7.2.0 dev: true + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + /istanbul-lib-source-maps@4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} @@ -14939,6 +15372,17 @@ packages: - supports-color dev: true + /istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-reports@3.1.5: resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} engines: {node: '>=8'} @@ -14947,6 +15391,14 @@ packages: istanbul-lib-report: 3.0.0 dev: true + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15711,6 +16163,10 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-types@1.0.0: resolution: {integrity: sha512-bfwqBW9cC/Lp7xcRpug7YrXm0IVw+T9e3g4mCYnv0Pjr3zIzU9PCQElYU9oSGAWzXlbdl9X5SAMPejO9sxkeUw==} engines: {node: '>=0.10.0'} @@ -16298,6 +16754,14 @@ packages: engines: {node: '>= 12.13.0'} dev: true + /local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + dependencies: + mlly: 1.6.1 + pkg-types: 1.0.3 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -16441,6 +16905,12 @@ packages: dependencies: js-tokens: 4.0.0 + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + dependencies: + get-func-name: 2.0.2 + dev: true + /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: @@ -16498,6 +16968,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /magicast@0.3.3: + resolution: {integrity: sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==} + dependencies: + '@babel/parser': 7.24.0 + '@babel/types': 7.24.0 + source-map-js: 1.0.2 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -16505,6 +16983,13 @@ packages: semver: 6.3.1 dev: true + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: true + /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true @@ -17160,6 +17645,15 @@ packages: hasBin: true dev: false + /mlly@1.6.1: + resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} + dependencies: + acorn: 8.11.3 + pathe: 1.1.2 + pkg-types: 1.0.3 + ufo: 1.5.2 + dev: true + /module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} dev: false @@ -17753,7 +18247,6 @@ packages: engines: {node: '>=18'} dependencies: yocto-queue: 1.0.0 - dev: false /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} @@ -17972,6 +18465,14 @@ packages: engines: {node: '>=8'} dev: true + /pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + dev: true + + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -18133,6 +18634,14 @@ packages: find-up: 5.0.0 dev: true + /pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + dependencies: + jsonc-parser: 3.2.0 + mlly: 1.6.1 + pathe: 1.1.2 + dev: true + /pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -19647,6 +20156,10 @@ packages: get-intrinsic: 1.1.3 object-inspect: 1.12.2 + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -19887,6 +20400,10 @@ packages: escape-string-regexp: 2.0.0 dev: true + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + /state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} dev: true @@ -19904,6 +20421,10 @@ packages: engines: {node: '>= 0.8'} dev: false + /std-env@3.7.0: + resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + dev: true + /stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -20069,6 +20590,12 @@ packages: engines: {node: '>=8'} dev: true + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} + dependencies: + js-tokens: 8.0.3 + dev: true + /strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} dev: false @@ -20548,10 +21075,24 @@ packages: globrex: 0.1.2 dev: true + /tinybench@2.6.0: + resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} + dev: true + /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} dev: true + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + dev: true + /titleize@4.0.0: resolution: {integrity: sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g==} engines: {node: '>=18'} @@ -20829,6 +21370,10 @@ packages: resolution: {integrity: sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==} dev: true + /ufo@1.5.2: + resolution: {integrity: sha512-eiutMaL0J2MKdhcOM1tUy13pIrYnyR87fEd8STJQFrrAwImwvlXkxlZEjaKah8r2viPohld08lt73QfLG1NxMg==} + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -21059,6 +21604,15 @@ packages: convert-source-map: 1.9.0 dev: true + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 2.0.0 + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -21105,6 +21659,119 @@ packages: vfile-message: 4.0.2 dev: true + /vite-node@1.4.0(@types/node@20.11.20): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.1.6(@types/node@20.11.20) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite@5.1.6(@types/node@20.11.20): + resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.20 + esbuild: 0.19.12 + postcss: 8.4.35 + rollup: 4.12.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@1.4.0(@types/node@20.11.20): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.11.20 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.7 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.0.0 + tinybench: 2.6.0 + tinypool: 0.8.2 + vite: 5.1.6(@types/node@20.11.20) + vite-node: 1.4.0(@types/node@20.11.20) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /void-elements@3.1.0: resolution: {integrity: sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=} engines: {node: '>=0.10.0'} @@ -21241,6 +21908,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'}