diff --git a/.github/workflows/alteration-compatibility-integration-test.yml b/.github/workflows/alteration-compatibility-integration-test.yml index 96cd0493c9c..f73c141d369 100644 --- a/.github/workflows/alteration-compatibility-integration-test.yml +++ b/.github/workflows/alteration-compatibility-integration-test.yml @@ -51,7 +51,7 @@ jobs: env: INTEGRATION_TEST: true steps: - - uses: logto-io/actions-package-logto-artifact@v1.1.0 + - uses: logto-io/actions-package-logto-artifact@v2 with: artifact-name: alteration-integration-test-${{ github.sha }} branch: ${{github.base_ref}} @@ -68,7 +68,7 @@ jobs: DB_URL: postgres://postgres:postgres@localhost:5432/postgres steps: - - uses: logto-io/actions-run-logto-integration-tests@v1.1.0 + - uses: logto-io/actions-run-logto-integration-tests@v2 with: branch: ${{github.base_ref}} logto_artifact: alteration-integration-test-${{ github.sha }} diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index ecc733d7904..0ee63394e9e 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -24,7 +24,8 @@ jobs: uses: silverhand-io/actions-node-pnpm-run-steps@v4 - name: Commitlint - run: npx commitlint --from HEAD~${{ github.event.pull_request.commits }} --to HEAD + # Credit to https://stackoverflow.com/a/67365254/12514940 + run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose - name: Commitlint on PR title run: echo '${{ github.event.pull_request.title }}' | npx commitlint diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 65b2bdfe414..6fcd3f321cd 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -18,7 +18,7 @@ jobs: INTEGRATION_TEST: true steps: - - uses: logto-io/actions-package-logto-artifact@v1.1.0 + - uses: logto-io/actions-package-logto-artifact@v2 with: artifact-name: integration-test-${{ github.sha }} @@ -34,7 +34,7 @@ jobs: DB_URL: postgres://postgres:postgres@localhost:5432/postgres steps: - - uses: logto-io/actions-run-logto-integration-tests@v1.1.0 + - uses: logto-io/actions-run-logto-integration-tests@v2 with: logto_artifact: integration-test-${{ github.sha }} test_target: ${{ matrix.target }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85383075915..b4f6eb2b88b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,7 +118,7 @@ jobs: run: ./.scripts/package.sh - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: token: ${{ secrets.BOT_PAT }} body: "" diff --git a/.gitignore b/.gitignore index 0c3f3abc97e..a95d6581e74 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ dump.rdb # connectors /packages/core/connectors + +# console auto generated files +/packages/console/src/consts/jwt-customizer-type-definition.ts diff --git a/.gitpod.yml b/.gitpod.yml index 0974dbdc813..31dcc8e32d6 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/README.md b/README.md index ae1e1281029..98fe9a272bd 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ Logto uses the [default browserslist config](https://github.com/browserslist/bro - About other bug reports, feature requests, and feedback, you can: - Directly 🙋 [open an issue](https://github.com/logto-io/logto/issues/new) on GitHub; - 💬 [join our Discord server](https://discord.gg/vRvwuwgpVX) to have a live chat; - - Engage in our 📍 [public roadmap](https://github.com/logto-io/logto/issues/1937). ## Licensing diff --git a/commitlint.config.cjs b/commitlint.config.ts similarity index 65% rename from commitlint.config.cjs rename to commitlint.config.ts index a46859ab2f3..5f44c75e5e8 100644 --- a/commitlint.config.cjs +++ b/commitlint.config.ts @@ -1,15 +1,17 @@ -const { rules } = require('@commitlint/config-conventional'); +import conventional from '@commitlint/config-conventional'; +import { UserConfig } from '@commitlint/types'; const isCi = process.env.CI === 'true'; -/** @type {import('@commitlint/types').UserConfig} **/ -module.exports = { +const config: UserConfig = { extends: ['@commitlint/config-conventional'], rules: { - 'type-enum': [2, 'always', [...rules['type-enum'][2], 'api', 'release']], + 'type-enum': [2, 'always', [...conventional.rules['type-enum'][2], 'api', 'release']], 'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'experience', 'deps', 'deps-dev', 'cli', 'toolkit', 'cloud', 'app-insights']], // Slightly increase the tolerance to allow the appending PR number ...(isCi && { 'header-max-length': [2, 'always', 110] }), 'body-max-line-length': [2, 'always', 110], }, }; + +export default config; diff --git a/package.json b/package.json index 292c0aa6989..d64d30c2cb7 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", @@ -25,9 +25,9 @@ }, "devDependencies": { "@changesets/cli": "^2.26.2", - "@commitlint/cli": "^18.0.0", - "@commitlint/config-conventional": "^18.0.0", - "@commitlint/types": "^18.0.0", + "@commitlint/cli": "^19.0.0", + "@commitlint/config-conventional": "^19.0.0", + "@commitlint/types": "^19.0.0", "@types/pg": "^8.6.6", "husky": "^9.0.0", "pg": "^8.8.0", diff --git a/packages/app-insights/jest.config.js b/packages/app-insights/jest.config.js deleted file mode 100644 index 5656d33479a..00000000000 --- a/packages/app-insights/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - transform: {}, - coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], - coverageReporters: ['text-summary', 'lcov'], - coverageProvider: 'v8', - roots: ['./lib'], - moduleNameMapper: { - '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', - }, -}; - -export default config; diff --git a/packages/app-insights/package.json b/packages/app-insights/package.json index 948f80f26bd..dc5229948e2 100644 --- a/packages/app-insights/package.json +++ b/packages/app-insights/package.json @@ -24,12 +24,12 @@ "scripts": { "precommit": "lint-staged", "build": "rm -rf lib/ && tsc -p tsconfig.build.json", - "build:test": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap", + "build:test": "pnpm build", "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": "vitest src", + "test:ci": "pnpm run test --silent --coverage", "prepack": "pnpm build" }, "devDependencies": { @@ -37,17 +37,16 @@ "@silverhand/eslint-config-react": "5.0.0", "@silverhand/ts-config": "5.0.0", "@silverhand/ts-config-react": "5.0.0", - "@types/jest": "^29.4.0", "@types/node": "^20.9.5", "@types/react": "^18.0.31", + "@vitest/coverage-v8": "^1.4.0", "eslint": "^8.44.0", "history": "^5.3.0", - "jest": "^29.7.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", "react": "^18.0.0", - "tslib": "^2.4.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^1.4.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/app-insights/src/normalize-error.test.ts b/packages/app-insights/src/normalize-error.test.ts index 798d7cac663..ab60124a021 100644 --- a/packages/app-insights/src/normalize-error.test.ts +++ b/packages/app-insights/src/normalize-error.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { normalizeError } from './normalize-error.js'; describe('normalizeError()', () => { diff --git a/packages/app-insights/tsconfig.json b/packages/app-insights/tsconfig.json index 83eac9c0ac5..2e988e53b2f 100644 --- a/packages/app-insights/tsconfig.json +++ b/packages/app-insights/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@silverhand/ts-config-react/tsconfig.base", "compilerOptions": { "outDir": "lib", - "types": ["node", "jest"], + "types": ["node"], }, "include": [ "src" diff --git a/packages/app-insights/tsconfig.test.json b/packages/app-insights/tsconfig.test.json deleted file mode 100644 index f30817b04be..00000000000 --- a/packages/app-insights/tsconfig.test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.build", - "compilerOptions": { - "isolatedModules": false, - "allowJs": true, - }, - "include": ["src"] -} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 25981db9de4..44a57e0f0c3 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## 1.15.0 + +### Patch Changes + +- Updated dependencies [5758f84f5] +- Updated dependencies [57d97a4df] +- Updated dependencies [abffb9f95] +- Updated dependencies [746483c49] +- Updated dependencies [2cbc591ff] +- Updated dependencies [57d97a4df] +- Updated dependencies [cc01acbd0] +- Updated dependencies [951865859] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/phrases@1.10.0 + - @logto/connector-kit@3.0.0 + - @logto/core-kit@2.4.0 + - @logto/schemas@1.15.0 + - @logto/phrases-experience@1.6.1 + - @logto/shared@3.1.0 + ## 1.14.0 ### Patch Changes diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js deleted file mode 100644 index 5656d33479a..00000000000 --- a/packages/cli/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - transform: {}, - coveragePathIgnorePatterns: ['/node_modules/', '/src/__mocks__/'], - coverageReporters: ['text-summary', 'lcov'], - coverageProvider: 'v8', - roots: ['./lib'], - moduleNameMapper: { - '^(chalk|inquirer)$': '/../shared/lib/esm/module-proxy.js', - }, -}; - -export default config; diff --git a/packages/cli/package.json b/packages/cli/package.json index ea0cec92490..be66801bc57 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@logto/cli", - "version": "1.14.0", + "version": "1.15.0", "description": "Logto CLI.", "author": "Silverhand Inc. ", "homepage": "https://github.com/logto-io/logto#readme", @@ -25,15 +25,14 @@ "precommit": "lint-staged", "prepare:package-json": "node -p \"'export const packageJson = ' + JSON.stringify(require('./package.json'), undefined, 2) + ';'\" > src/package-json.ts", "build": "rm -rf lib && pnpm prepare:package-json && tsc -p tsconfig.build.json", - "build:test": "rm -rf lib/ && pnpm prepare:package-json && tsc -p tsconfig.test.json --sourcemap", + "build:test": "pnpm build", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental", "start": "node .", "start:dev": "pnpm build && node .", "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", + "test": "vitest src", + "test:ci": "pnpm run test --silent --coverage", "prepack": "pnpm build" }, "engines": { @@ -43,14 +42,15 @@ "url": "https://github.com/logto-io/logto/issues" }, "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", - "@logto/core-kit": "workspace:^2.3.0", + "@logto/connector-kit": "workspace:^3.0.0", + "@logto/core-kit": "workspace:^2.4.0", "@logto/language-kit": "workspace:^1.1.0", - "@logto/phrases": "workspace:^1.9.0", - "@logto/phrases-experience": "workspace:^1.6.0", - "@logto/schemas": "workspace:1.14.0", + "@logto/phrases": "workspace:^1.10.0", + "@logto/phrases-experience": "workspace:^1.6.1", + "@logto/schemas": "workspace:1.15.0", "@logto/shared": "workspace:^3.1.0", "@silverhand/essentials": "^2.9.0", + "@silverhand/slonik": "31.0.0-beta.2", "chalk": "^5.0.0", "decamelize": "^6.0.0", "dotenv": "^16.0.0", @@ -65,9 +65,6 @@ "pg-protocol": "^1.6.0", "roarr": "^7.11.0", "semver": "^7.3.8", - "slonik": "^30.0.0", - "slonik-interceptor-preset": "^1.2.10", - "slonik-sql-tag-raw": "^1.1.4", "tar": "^6.1.11", "typescript": "^5.3.3", "yargs": "^17.6.0", @@ -76,19 +73,19 @@ "devDependencies": { "@silverhand/eslint-config": "5.0.0", "@silverhand/ts-config": "5.0.0", - "@withtyped/server": "^0.12.9", "@types/inquirer": "^9.0.0", - "@types/jest": "^29.4.0", "@types/node": "^20.9.5", "@types/semver": "^7.3.12", "@types/sinon": "^17.0.0", "@types/tar": "^6.1.2", "@types/yargs": "^17.0.13", + "@vitest/coverage-v8": "^1.4.0", + "@withtyped/server": "^0.13.3", "eslint": "^8.44.0", - "jest": "^29.7.0", "lint-staged": "^15.0.0", "prettier": "^3.0.0", - "sinon": "^17.0.0" + "sinon": "^17.0.0", + "vitest": "^1.4.0" }, "eslintConfig": { "extends": "@silverhand", diff --git a/packages/cli/src/commands/database/alteration/index.test.ts b/packages/cli/src/commands/database/alteration/index.test.ts index db289c21a7a..47dbcf02a5e 100644 --- a/packages/cli/src/commands/database/alteration/index.test.ts +++ b/packages/cli/src/commands/database/alteration/index.test.ts @@ -1,14 +1,11 @@ -import { createMockUtils } from '@logto/shared/esm'; +import { createMockPool } from '@silverhand/slonik'; import Sinon from 'sinon'; -import { createMockPool } from 'slonik'; +import { vi, expect, afterAll, describe, it } from 'vitest'; import { chooseAlterationsByVersion } from './version.js'; -const { jest } = import.meta; -const { mockEsmWithActual } = createMockUtils(jest); - const pool = createMockPool({ - query: jest.fn(), + query: vi.fn(), }); const files = Object.freeze([ @@ -17,16 +14,19 @@ const files = Object.freeze([ { filename: '1.0.0-1663923772-c.js', path: '/alterations-js/1.0.0-1663923772-c.js' }, ]); -await mockEsmWithActual('./utils.js', () => ({ +vi.mock('./utils.js', async (importOriginal) => ({ + // eslint-disable-next-line @typescript-eslint/ban-types + ...(await importOriginal()), getAlterationFiles: async () => files, })); -const { getCurrentDatabaseAlterationTimestamp } = await mockEsmWithActual( - '../../../queries/system.js', - () => ({ - getCurrentDatabaseAlterationTimestamp: jest.fn(), - }) -); +const getCurrentDatabaseAlterationTimestamp = vi.fn(); + +vi.doMock('../../../queries/system.js', async (importOriginal) => ({ + // eslint-disable-next-line @typescript-eslint/ban-types + ...(await importOriginal()), + getCurrentDatabaseAlterationTimestamp, +})); const { getAvailableAlterations } = await import('./index.js'); diff --git a/packages/cli/src/commands/database/alteration/index.ts b/packages/cli/src/commands/database/alteration/index.ts index 03a10324376..16b47bd533f 100644 --- a/packages/cli/src/commands/database/alteration/index.ts +++ b/packages/cli/src/commands/database/alteration/index.ts @@ -1,7 +1,7 @@ import type { AlterationScript } from '@logto/schemas/lib/types/alteration.js'; import { conditionalString } from '@silverhand/essentials'; +import type { DatabasePool } from '@silverhand/slonik'; import chalk from 'chalk'; -import type { DatabasePool } from 'slonik'; import type { CommandModule } from 'yargs'; import { createPoolFromConfig } from '../../../database.js'; diff --git a/packages/cli/src/commands/database/ogcio/applications.ts b/packages/cli/src/commands/database/ogcio/applications.ts index 59446af2df9..a6630977417 100644 --- a/packages/cli/src/commands/database/ogcio/applications.ts +++ b/packages/cli/src/commands/database/ogcio/applications.ts @@ -4,7 +4,7 @@ /* eslint-disable @silverhand/fp/no-mutation */ import { ApplicationType } from '@logto/schemas'; import { generateStandardSecret } from '@logto/shared'; -import { sql, type DatabaseTransactionConnection } from 'slonik'; +import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { type OgcioParams } from './index.js'; import { createItem } from './queries.js'; diff --git a/packages/cli/src/commands/database/ogcio/ogcio.ts b/packages/cli/src/commands/database/ogcio/ogcio.ts index 11b2edbae7a..e88e2d12141 100644 --- a/packages/cli/src/commands/database/ogcio/ogcio.ts +++ b/packages/cli/src/commands/database/ogcio/ogcio.ts @@ -3,7 +3,7 @@ /* eslint-disable @silverhand/fp/no-let */ import { defaultTenantId } from '@logto/schemas'; -import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik'; +import type { CommonQueryMethods, DatabaseTransactionConnection } from '@silverhand/slonik'; import { seedApplications } from './applications.js'; import { type OgcioParams } from './index.js'; diff --git a/packages/cli/src/commands/database/ogcio/organizations-rbac.ts b/packages/cli/src/commands/database/ogcio/organizations-rbac.ts index 60829a8da04..c82c6697a2b 100644 --- a/packages/cli/src/commands/database/ogcio/organizations-rbac.ts +++ b/packages/cli/src/commands/database/ogcio/organizations-rbac.ts @@ -3,7 +3,7 @@ /* eslint-disable @silverhand/fp/no-mutating-methods */ /* eslint-disable @silverhand/fp/no-mutation */ import { OrganizationScopes, OrganizationRoles } from '@logto/schemas'; -import { sql, type DatabaseTransactionConnection } from 'slonik'; +import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { createItem, createItemWithoutId } from './queries.js'; diff --git a/packages/cli/src/commands/database/ogcio/organizations.ts b/packages/cli/src/commands/database/ogcio/organizations.ts index b06fea5e686..ca3a85e6951 100644 --- a/packages/cli/src/commands/database/ogcio/organizations.ts +++ b/packages/cli/src/commands/database/ogcio/organizations.ts @@ -1,4 +1,4 @@ -import { type DatabaseTransactionConnection, sql } from 'slonik'; +import { type DatabaseTransactionConnection, sql } from '@silverhand/slonik'; import { createItem } from './queries.js'; diff --git a/packages/cli/src/commands/database/ogcio/queries.ts b/packages/cli/src/commands/database/ogcio/queries.ts index 76dd668506c..6db110c4672 100644 --- a/packages/cli/src/commands/database/ogcio/queries.ts +++ b/packages/cli/src/commands/database/ogcio/queries.ts @@ -9,7 +9,7 @@ import { type QueryResult, sql, type ValueExpression, -} from 'slonik'; +} from '@silverhand/slonik'; import { insertInto } from '../../../database.js'; import { consoleLog } from '../../../utils.js'; diff --git a/packages/cli/src/commands/database/ogcio/resources-rbac.ts b/packages/cli/src/commands/database/ogcio/resources-rbac.ts index f9c2daf809a..05f0f4b2a0a 100644 --- a/packages/cli/src/commands/database/ogcio/resources-rbac.ts +++ b/packages/cli/src/commands/database/ogcio/resources-rbac.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @silverhand/fp/no-mutating-methods */ /* eslint-disable @silverhand/fp/no-mutation */ -import { sql, type DatabaseTransactionConnection } from 'slonik'; +import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { createItem } from './queries.js'; diff --git a/packages/cli/src/commands/database/ogcio/resources.ts b/packages/cli/src/commands/database/ogcio/resources.ts index d61de195a8e..6210bfcd14d 100644 --- a/packages/cli/src/commands/database/ogcio/resources.ts +++ b/packages/cli/src/commands/database/ogcio/resources.ts @@ -1,6 +1,6 @@ /* eslint-disable eslint-comments/disable-enable-pair */ -import { sql, type DatabaseTransactionConnection } from 'slonik'; +import { sql, type DatabaseTransactionConnection } from '@silverhand/slonik'; import { createItem } from './queries.js'; diff --git a/packages/cli/src/commands/database/seed/cloud.ts b/packages/cli/src/commands/database/seed/cloud.ts index a6cc6446d60..9dc8194a9a4 100644 --- a/packages/cli/src/commands/database/seed/cloud.ts +++ b/packages/cli/src/commands/database/seed/cloud.ts @@ -9,8 +9,8 @@ import { } from '@logto/schemas'; import { GlobalValues } from '@logto/shared'; import { appendPath } from '@silverhand/essentials'; -import type { CommonQueryMethods } from 'slonik'; -import { sql } from 'slonik'; +import type { CommonQueryMethods } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; import { insertInto } from '../../../database.js'; import { consoleLog } from '../../../utils.js'; diff --git a/packages/cli/src/commands/database/seed/index.ts b/packages/cli/src/commands/database/seed/index.ts index 282c1708754..84042b5ae70 100644 --- a/packages/cli/src/commands/database/seed/index.ts +++ b/packages/cli/src/commands/database/seed/index.ts @@ -1,4 +1,4 @@ -import type { DatabasePool } from 'slonik'; +import type { DatabasePool } from '@silverhand/slonik'; import type { CommandModule } from 'yargs'; import { createPoolAndDatabaseIfNeeded } from '../../../database.js'; diff --git a/packages/cli/src/commands/database/seed/oidc-config.ts b/packages/cli/src/commands/database/seed/oidc-config.ts index 40334222845..bc0308ef701 100644 --- a/packages/cli/src/commands/database/seed/oidc-config.ts +++ b/packages/cli/src/commands/database/seed/oidc-config.ts @@ -4,8 +4,8 @@ import type { LogtoOidcConfigType } from '@logto/schemas'; import { LogtoOidcConfigKey, logtoConfigGuards } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { getEnvAsStringArray } from '@silverhand/essentials'; +import type { DatabaseTransactionConnection } from '@silverhand/slonik'; import chalk from 'chalk'; -import type { DatabaseTransactionConnection } from 'slonik'; import { z } from 'zod'; import { getRowsByKeys, updateValueByKey } from '../../../queries/logto-config.js'; diff --git a/packages/cli/src/commands/database/seed/tables.ts b/packages/cli/src/commands/database/seed/tables.ts index 33723c08081..de4dc07f84c 100644 --- a/packages/cli/src/commands/database/seed/tables.ts +++ b/packages/cli/src/commands/database/seed/tables.ts @@ -28,14 +28,14 @@ import { } from '@logto/schemas'; import { getTenantRole } from '@logto/schemas'; import { Tenants } from '@logto/schemas/models'; -import { convertToIdentifiers, generateStandardId } from '@logto/shared'; -import type { DatabaseTransactionConnection } from 'slonik'; -import { sql } from 'slonik'; -import { raw } from 'slonik-sql-tag-raw'; +import { generateStandardId } from '@logto/shared'; +import type { DatabaseTransactionConnection } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; import { insertInto } from '../../../database.js'; import { getDatabaseName } from '../../../queries/database.js'; import { updateDatabaseTimestamp } from '../../../queries/system.js'; +import { convertToIdentifiers } from '../../../sql.js'; import { consoleLog, getPathInModule } from '../../../utils.js'; import { appendAdminConsoleRedirectUris, seedTenantCloudServiceApplication } from './cloud.js'; @@ -104,7 +104,7 @@ export const createTables = async ( if (query) { await connection.query( - sql`${raw( + sql`${sql.raw( /* eslint-disable no-template-curly-in-string */ query .replaceAll('${name}', parameters.name ?? '') @@ -128,7 +128,7 @@ export const createTables = async ( /* eslint-disable no-await-in-loop */ for (const [file, query] of sorted) { - await connection.query(sql`${raw(query)}`); + await connection.query(sql`${sql.raw(query)}`); if (!query.includes('/* no_after_each */')) { await runLifecycleQuery('after_each', { name: file.split('.')[0], database }); @@ -168,7 +168,12 @@ export const seedTables = async ( adminTenantId, applicationRole.id, ...cloudAdditionalScopes - .filter(({ name }) => name === CloudScope.SendSms || name === CloudScope.SendEmail) + .filter( + ({ name }) => + name === CloudScope.SendSms || + name === CloudScope.SendEmail || + name === CloudScope.FetchCustomJwt + ) .map(({ id }) => id) ); @@ -186,11 +191,18 @@ export const seedTables = async ( ), connection.query(insertInto(createAdminTenantSignInExperience(), SignInExperiences.table)), connection.query(insertInto(createDefaultAdminConsoleApplication(), Applications.table)), - updateDatabaseTimestamp(connection, latestTimestamp), + ]); + + // The below seed data is for the Logto Cloud only. We put it here for the sack of simplicity. + // The data is not harmful for OSS, since they are all admin tenant data. OSS will not use them + // and they cannot be seen by the Console. + await Promise.all([ seedTenantOrganizations(connection), seedManagementApiProxyApplications(connection), ]); + await updateDatabaseTimestamp(connection, latestTimestamp); + consoleLog.succeed('Seed data'); }; diff --git a/packages/cli/src/commands/database/seed/tenant-organizations.ts b/packages/cli/src/commands/database/seed/tenant-organizations.ts index 01c99a1b967..ab6a4aaa127 100644 --- a/packages/cli/src/commands/database/seed/tenant-organizations.ts +++ b/packages/cli/src/commands/database/seed/tenant-organizations.ts @@ -12,7 +12,7 @@ import { getTenantOrganizationCreateData, Organizations, } from '@logto/schemas'; -import type { DatabaseTransactionConnection } from 'slonik'; +import type { DatabaseTransactionConnection } from '@silverhand/slonik'; import { insertInto } from '../../../database.js'; import { consoleLog } from '../../../utils.js'; diff --git a/packages/cli/src/commands/database/seed/tenant.ts b/packages/cli/src/commands/database/seed/tenant.ts index 0a2f9ca470e..092fe3a8a40 100644 --- a/packages/cli/src/commands/database/seed/tenant.ts +++ b/packages/cli/src/commands/database/seed/tenant.ts @@ -17,9 +17,8 @@ import { } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; import { assert } from '@silverhand/essentials'; -import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik'; -import { sql } from 'slonik'; -import { raw } from 'slonik-sql-tag-raw'; +import type { CommonQueryMethods, DatabaseTransactionConnection } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; import { insertInto } from '../../../database.js'; import { getDatabaseName } from '../../../queries/database.js'; @@ -37,7 +36,7 @@ export const createTenant = async (pool: CommonQueryMethods, tenantId: string) = await pool.query(insertInto(createTenant, 'tenants')); await pool.query(sql` create role ${sql.identifier([role])} with inherit login - password '${raw(password)}' + password '${sql.raw(password)}' in role ${sql.identifier([parentRole])}; `); }; diff --git a/packages/cli/src/commands/translate/openai.ts b/packages/cli/src/commands/translate/openai.ts index 551465d459a..26e30efb20d 100644 --- a/packages/cli/src/commands/translate/openai.ts +++ b/packages/cli/src/commands/translate/openai.ts @@ -14,7 +14,7 @@ export const createOpenaiApi = () => { const proxy = getProxy(); return got.extend({ - prefixUrl: 'https://api.openai.com/v1', + prefixUrl: process.env.OPENAI_API_PROXY_ENDPOINT ?? 'https://api.openai.com/v1', headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ''}`, }, @@ -50,7 +50,8 @@ export const translate = async ({ api .post('chat/completions', { json: { - model: 'gpt-3.5-turbo-1106', + // The full list of OPENAI model can be found at https://platform.openai.com/docs/models. + model: process.env.OPENAI_MODEL_NAME ?? 'gpt-3.5-turbo-0125', messages: getTranslationPromptMessages({ sourceFileContent, targetLanguage, diff --git a/packages/cli/src/connector/factories.ts b/packages/cli/src/connector/factories.ts index 50281a72673..f907657edb8 100644 --- a/packages/cli/src/connector/factories.ts +++ b/packages/cli/src/connector/factories.ts @@ -8,8 +8,9 @@ import { loadConnector } from './loader.js'; import type { ConnectorFactory, ConnectorPackage } from './types.js'; import { parseMetadata, validateConnectorModule } from './utils.js'; -// eslint-disable-next-line @silverhand/fp/no-let, @typescript-eslint/no-explicit-any -let cachedConnectorFactories: Array>> | undefined; +// eslint-disable-next-line @silverhand/fp/no-let +let cachedConnectorFactories: // eslint-disable-next-line @typescript-eslint/no-explicit-any +Array>> | undefined; export const loadConnectorFactories = async ( connectorPackages: ConnectorPackage[], @@ -49,8 +50,10 @@ export const loadConnectorFactories = async ( // eslint-disable-next-line @silverhand/fp/no-mutation cachedConnectorFactories = connectorFactories.filter( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (connectorFactory): connectorFactory is ConnectorFactory> => + ( + connectorFactory + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): connectorFactory is ConnectorFactory> => connectorFactory !== undefined ); diff --git a/packages/cli/src/connector/types.ts b/packages/cli/src/connector/types.ts index 2c296f545c8..e6c1287dbb5 100644 --- a/packages/cli/src/connector/types.ts +++ b/packages/cli/src/connector/types.ts @@ -6,7 +6,7 @@ import type { BaseRoutes, Router } from '@withtyped/server'; */ export type ConnectorFactory< // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends Router, + T extends Router, U extends AllConnector = AllConnector, > = Pick & { createConnector: CreateConnector; diff --git a/packages/cli/src/connector/utils.ts b/packages/cli/src/connector/utils.ts index c0f2c4ca666..7a1c612069b 100644 --- a/packages/cli/src/connector/utils.ts +++ b/packages/cli/src/connector/utils.ts @@ -93,7 +93,7 @@ export const parseMetadata = async ( export const buildRawConnector = async < // eslint-disable-next-line @typescript-eslint/no-explicit-any - T extends Router, + T extends Router, U extends AllConnector = AllConnector, >( connectorFactory: ConnectorFactory, diff --git a/packages/cli/src/database.ts b/packages/cli/src/database.ts index 26ca45a733d..f639ce8231a 100644 --- a/packages/cli/src/database.ts +++ b/packages/cli/src/database.ts @@ -1,11 +1,16 @@ import type { SchemaLike } from '@logto/schemas'; -import { convertToPrimitiveOrSql } from '@logto/shared'; import { assert } from '@silverhand/essentials'; +import { + createPool, + parseDsn, + sql, + stringifyDsn, + createInterceptorsPreset, +} from '@silverhand/slonik'; import decamelize from 'decamelize'; import { DatabaseError } from 'pg-protocol'; -import { createPool, parseDsn, sql, stringifyDsn } from 'slonik'; -import { createInterceptors } from 'slonik-interceptor-preset'; +import { convertToPrimitiveOrSql } from './sql.js'; import { ConfigKey, consoleLog, getCliConfigWithPrompt } from './utils.js'; export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto'; @@ -22,7 +27,7 @@ export const createPoolFromConfig = async () => { assert(parseDsn(databaseUrl).databaseName, new Error('Database name is required in URL')); return createPool(databaseUrl, { - interceptors: createInterceptors(), + interceptors: createInterceptorsPreset(), }); }; @@ -49,7 +54,7 @@ export const createPoolAndDatabaseIfNeeded = async () => { // - It will throw error when creating database using '?' const databaseName = dsn.databaseName ?? '?'; const maintenancePool = await createPool(stringifyDsn({ ...dsn, databaseName: 'postgres' }), { - interceptors: createInterceptors(), + interceptors: createInterceptorsPreset(), }); await maintenancePool.query(sql` create database ${sql.identifier([databaseName])} diff --git a/packages/cli/src/include.d/import-meta.d.ts b/packages/cli/src/include.d/import-meta.d.ts deleted file mode 100644 index 7a1591f98d2..00000000000 --- a/packages/cli/src/include.d/import-meta.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -interface ImportMeta { - // By TypeScript design we must use `import()` - // eslint-disable-next-line @typescript-eslint/consistent-type-imports - jest: typeof jest & import('@logto/shared/esm').WithEsmMock; -} diff --git a/packages/cli/src/include.d/slonik-interceptor-preset.d.ts b/packages/cli/src/include.d/slonik-interceptor-preset.d.ts deleted file mode 100644 index 778cb40c066..00000000000 --- a/packages/cli/src/include.d/slonik-interceptor-preset.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module 'slonik-interceptor-preset' { - import type { Interceptor } from 'slonik'; - - export const createInterceptors: (config?: { - benchmarkQueries: boolean; - logQueries: boolean; - normaliseQueries: boolean; - transformFieldNames: boolean; - }) => readonly Interceptor[]; -} diff --git a/packages/cli/src/queries/database.ts b/packages/cli/src/queries/database.ts index 5ba83752cc2..2f8f715a43c 100644 --- a/packages/cli/src/queries/database.ts +++ b/packages/cli/src/queries/database.ts @@ -1,5 +1,5 @@ -import type { CommonQueryMethods } from 'slonik'; -import { sql } from 'slonik'; +import type { CommonQueryMethods } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; export const getDatabaseName = async (pool: CommonQueryMethods, normalized = false) => { const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql` diff --git a/packages/cli/src/queries/logto-config.ts b/packages/cli/src/queries/logto-config.ts index a0912711b0d..1abca8fb196 100644 --- a/packages/cli/src/queries/logto-config.ts +++ b/packages/cli/src/queries/logto-config.ts @@ -1,11 +1,12 @@ import type { LogtoConfig, LogtoConfigKey, logtoConfigGuards } from '@logto/schemas'; import { LogtoConfigs } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; -import type { CommonQueryMethods } from 'slonik'; -import { sql } from 'slonik'; +import type { CommonQueryMethods } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; import type { z } from 'zod'; +import { convertToIdentifiers } from '../sql.js'; + const { table, fields } = convertToIdentifiers(LogtoConfigs); export const doesConfigsTableExist = async (pool: CommonQueryMethods) => { diff --git a/packages/cli/src/queries/system.test.ts b/packages/cli/src/queries/system.test.ts index 8eda604e29d..15058d322fd 100644 --- a/packages/cli/src/queries/system.test.ts +++ b/packages/cli/src/queries/system.test.ts @@ -1,16 +1,15 @@ import { AlterationStateKey, Systems } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; +import { createMockPool, createMockQueryResult, sql } from '@silverhand/slonik'; import { DatabaseError } from 'pg-protocol'; -import { createMockPool, createMockQueryResult, sql } from 'slonik'; +import { describe, it, expect, vi, type MockedFunction, afterAll, beforeAll } from 'vitest'; +import { convertToIdentifiers } from '../sql.js'; import type { QueryType } from '../test-utils.js'; import { expectSqlAssert } from '../test-utils.js'; import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './system.js'; -const { jest } = import.meta; - -const mockQuery: jest.MockedFunction = jest.fn(); +const mockQuery: MockedFunction = vi.fn(); const pool = createMockPool({ query: async (sql, values) => { @@ -94,12 +93,12 @@ describe('updateDatabaseTimestamp()', () => { const updatedAt = '2022-09-21T06:32:46.583Z'; beforeAll(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(updatedAt)); + vi.useFakeTimers(); + vi.setSystemTime(new Date(updatedAt)); }); afterAll(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('sends upsert sql with timestamp and updatedAt', async () => { diff --git a/packages/cli/src/queries/system.ts b/packages/cli/src/queries/system.ts index 9b54907eb63..e6d274bcc24 100644 --- a/packages/cli/src/queries/system.ts +++ b/packages/cli/src/queries/system.ts @@ -1,12 +1,13 @@ import type { AlterationState, System, SystemKey } from '@logto/schemas'; import { systemGuards, Systems, AlterationStateKey } from '@logto/schemas'; -import { convertToIdentifiers } from '@logto/shared'; import type { Nullable } from '@silverhand/essentials'; +import type { CommonQueryMethods, DatabaseTransactionConnection } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; import { DatabaseError } from 'pg-protocol'; -import type { CommonQueryMethods, DatabaseTransactionConnection } from 'slonik'; -import { sql } from 'slonik'; import type { z } from 'zod'; +import { convertToIdentifiers } from '../sql.js'; + const { fields, table } = convertToIdentifiers(Systems); const doesTableExist = async (pool: CommonQueryMethods, table: string) => { diff --git a/packages/cli/src/sql.ts b/packages/cli/src/sql.ts new file mode 100644 index 00000000000..a7f23157ac4 --- /dev/null +++ b/packages/cli/src/sql.ts @@ -0,0 +1,75 @@ +/** + * @fileoverview Copied from `@logto/core`. Originally we put them in `@logto/shared` but it + * requires `slonik` which makes the package too heavy. + * + * Since `@logto/cli` only use these functions in a stable manner, we copy them here for now. If + * the number of functions grows, we should consider moving them to a separate package. (Actually, + * we should remove the dependency on `slonik` at all, and this may not be an issue then.) + */ + +import { type SchemaValue, type SchemaValuePrimitive, type Table } from '@logto/shared'; +import { type IdentifierSqlToken, type SqlToken, sql } from '@silverhand/slonik'; + +/** + * Note `undefined` is removed from the acceptable list, + * since you should NOT call this function if ignoring the field is the desired behavior. + * Calling this function with `null` means an explicit `null` setting in database is expected. + * @param key The key of value. Will treat as `timestamp` if it ends with `_at` or 'At' AND value is a number; + * @param value The value to convert. + * @returns A primitive that can be saved into database. + */ +export const convertToPrimitiveOrSql = ( + key: string, + value: SchemaValue + // eslint-disable-next-line @typescript-eslint/ban-types +): NonNullable | SqlToken | null => { + if (value === null) { + return null; + } + + if (typeof value === 'object') { + return JSON.stringify(value); + } + + if ( + (['_at', 'At'].some((value) => key.endsWith(value)) || key === 'date') && + typeof value === 'number' + ) { + return sql`to_timestamp(${value}::double precision / 1000)`; + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (typeof value === 'string') { + if (value === '') { + return null; + } + + return value; + } + + throw new Error(`Cannot convert ${key} to primitive`); +}; + +type FieldIdentifiers = { + [key in Key]: IdentifierSqlToken; +}; + +export const convertToIdentifiers = ( + { table, fields }: Table, + withPrefix = false +) => { + const fieldsIdentifiers = Object.entries(fields).map<[Key, IdentifierSqlToken]>( + // eslint-disable-next-line no-restricted-syntax -- Object.entries can only return string keys + ([key, value]) => [key as Key, sql.identifier(withPrefix ? [table, value] : [value])] + ); + + return { + table: sql.identifier([table]), + // Key value inferred from the original fields directly + // eslint-disable-next-line no-restricted-syntax + fields: Object.fromEntries(fieldsIdentifiers) as FieldIdentifiers, + }; +}; diff --git a/packages/cli/src/test-utils.ts b/packages/cli/src/test-utils.ts index ce1310ed334..67dfee35f18 100644 --- a/packages/cli/src/test-utils.ts +++ b/packages/cli/src/test-utils.ts @@ -1,7 +1,8 @@ // Copied from core -import type { QueryResult, QueryResultRow } from 'slonik'; -import type { PrimitiveValueExpression } from 'slonik/dist/src/types.js'; +import type { QueryResult, QueryResultRow } from '@silverhand/slonik'; +import type { PrimitiveValueExpression } from '@silverhand/slonik/dist/src/types.js'; +import { expect } from 'vitest'; export type QueryType = ( sql: string, diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 561ecb995f0..c4b6045bf6f 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@silverhand/ts-config/tsconfig.base", "compilerOptions": { "outDir": "lib", - "types": ["node", "jest"] + "types": ["node"] }, "include": [ "src" diff --git a/packages/cli/tsconfig.test.json b/packages/cli/tsconfig.test.json deleted file mode 100644 index 55de18c33aa..00000000000 --- a/packages/cli/tsconfig.test.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "isolatedModules": false, - "allowJs": true - }, - "include": ["src"] -} diff --git a/packages/connectors/.gitignore b/packages/connectors/.gitignore index 35dfa630e50..65245801929 100644 --- a/packages/connectors/.gitignore +++ b/packages/connectors/.gitignore @@ -1,8 +1,7 @@ # generated files -/*/types /*/tsconfig.* -/*/jest.config.* /*/rollup.config.* +/*/vitest.config.* # keep templates !/templates/** diff --git a/packages/connectors/connector-alipay-native/CHANGELOG.md b/packages/connectors/connector-alipay-native/CHANGELOG.md index ca6de109843..6e84bd0e814 100644 --- a/packages/connectors/connector-alipay-native/CHANGELOG.md +++ b/packages/connectors/connector-alipay-native/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-alipay-native +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-alipay-native/package.json b/packages/connectors/connector-alipay-native/package.json index df22a902b7c..b490e627503 100644 --- a/packages/connectors/connector-alipay-native/package.json +++ b/packages/connectors/connector-alipay-native/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-alipay-native", - "version": "1.1.0", + "version": "1.2.0", "description": "Alipay Native implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "dayjs": "^1.10.5", "iconv-lite": "^0.6.3" }, @@ -29,9 +29,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": { diff --git a/packages/connectors/connector-alipay-native/src/index.test.ts b/packages/connectors/connector-alipay-native/src/index.test.ts index 4ec5f4c426d..634a41e6b4e 100644 --- a/packages/connectors/connector-alipay-native/src/index.test.ts +++ b/packages/connectors/connector-alipay-native/src/index.test.ts @@ -6,13 +6,11 @@ import { alipayEndpoint } from './constant.js'; import createConnector, { getAccessToken } from './index.js'; import { mockedAlipayNativeConfigWithValidPrivateKey } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedAlipayNativeConfigWithValidPrivateKey); +const getConfig = vi.fn().mockResolvedValue(mockedAlipayNativeConfigWithValidPrivateKey); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by state', async () => { @@ -26,7 +24,7 @@ describe('getAuthorizationUri', () => { jti: 'dummy-jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toBe('alipay://?app_id=2021000000000000&state=dummy-state'); }); @@ -35,7 +33,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const alipayEndpointUrl = new URL(alipayEndpoint); @@ -72,7 +70,7 @@ describe('getAccessToken', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); @@ -93,7 +91,7 @@ describe('getAccessToken', () => { }); await expect( getAccessToken('code', mockedAlipayNativeConfigWithValidPrivateKey) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); it('should fail with wrong code', async () => { @@ -110,7 +108,7 @@ describe('getAccessToken', () => { }); await expect( getAccessToken('wrong_code', mockedAlipayNativeConfigWithValidPrivateKey) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code') ); }); @@ -136,7 +134,7 @@ describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const alipayEndpointUrl = new URL(alipayEndpoint); @@ -156,10 +154,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' }, + vi.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 () => { @@ -176,9 +187,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token') ); }); @@ -197,9 +206,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code') ); }); @@ -218,9 +225,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'Invalid parameter', code: '40002', @@ -245,14 +250,14 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.InvalidResponse)); + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( + new ConnectorError(ConnectorErrorCodes.InvalidResponse) + ); }); it('should throw with other request errors', async () => { nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-alipay-native/src/index.ts b/packages/connectors/connector-alipay-native/src/index.ts index fa412d68fab..dc7d216ebbd 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-native/src/utils.test.ts b/packages/connectors/connector-alipay-native/src/utils.test.ts index 727f1d52cd0..053a18b8216 100644 --- a/packages/connectors/connector-alipay-native/src/utils.test.ts +++ b/packages/connectors/connector-alipay-native/src/utils.test.ts @@ -5,14 +5,12 @@ import { } from './mock.js'; import { signingParameters } from './utils.js'; -const { jest } = import.meta; - -const listenJSONParse = jest.spyOn(JSON, 'parse'); -const listenJSONStringify = jest.spyOn(JSON, 'stringify'); +const listenJSONParse = vi.spyOn(JSON, 'parse'); +const listenJSONStringify = vi.spyOn(JSON, 'stringify'); describe('signingParameters', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); const testingParameters = { diff --git a/packages/connectors/connector-alipay-web/CHANGELOG.md b/packages/connectors/connector-alipay-web/CHANGELOG.md index af8302d3896..d2682545a91 100644 --- a/packages/connectors/connector-alipay-web/CHANGELOG.md +++ b/packages/connectors/connector-alipay-web/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-alipay-web +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-alipay-web/package.json b/packages/connectors/connector-alipay-web/package.json index b23babe622f..2dcc0397c42 100644 --- a/packages/connectors/connector-alipay-web/package.json +++ b/packages/connectors/connector-alipay-web/package.json @@ -1,9 +1,9 @@ { "name": "@logto/connector-alipay-web", - "version": "1.2.0", + "version": "1.3.0", "description": "Alipay implementation.", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "dayjs": "^1.10.5", "iconv-lite": "^0.6.3" }, @@ -28,9 +28,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": { diff --git a/packages/connectors/connector-alipay-web/src/index.test.ts b/packages/connectors/connector-alipay-web/src/index.test.ts index 2e0ad24fa52..caef90e611a 100644 --- a/packages/connectors/connector-alipay-web/src/index.test.ts +++ b/packages/connectors/connector-alipay-web/src/index.test.ts @@ -6,13 +6,11 @@ import { alipayEndpoint, authorizationEndpoint } from './constant.js'; import createConnector, { getAccessToken } from './index.js'; import { mockedAlipayConfigWithValidPrivateKey } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedAlipayConfigWithValidPrivateKey); +const getConfig = vi.fn().mockResolvedValue(mockedAlipayConfigWithValidPrivateKey); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { @@ -26,7 +24,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?app_id=2021000000000000&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&scope=auth_user&state=some_state` @@ -37,7 +35,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const alipayEndpointUrl = new URL(alipayEndpoint); @@ -78,7 +76,7 @@ describe('getAccessToken', () => { await expect( getAccessToken('code', mockedAlipayConfigWithValidPrivateKey) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); it('should fail with wrong code', async () => { @@ -96,7 +94,7 @@ describe('getAccessToken', () => { await expect( getAccessToken('wrong_code', mockedAlipayConfigWithValidPrivateKey) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid code') ); }); @@ -105,7 +103,7 @@ describe('getAccessToken', () => { describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); beforeEach(() => { @@ -142,15 +140,28 @@ 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' }, + vi.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 () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}') ); }); @@ -169,9 +180,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'Invalid auth token') ); }); @@ -190,9 +199,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'Invalid auth code') ); }); @@ -211,9 +218,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect( - connector.getUserInfo({ auth_code: 'wrong_code' }, jest.fn()) - ).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'wrong_code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'Invalid parameter', code: '40002', @@ -238,7 +243,7 @@ describe('getUserInfo', () => { sign: '', }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ auth_code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ auth_code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.InvalidResponse) ); }); @@ -246,6 +251,6 @@ describe('getUserInfo', () => { it('should throw with other request errors', async () => { nock(alipayEndpointUrl.origin).post(alipayEndpointUrl.pathname).query(true).reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ auth_code: 'code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ auth_code: 'code' }, vi.fn())).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-alipay-web/src/index.ts b/packages/connectors/connector-alipay-web/src/index.ts index 0329e5a4e55..c3f592e444b 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-alipay-web/src/utils.test.ts b/packages/connectors/connector-alipay-web/src/utils.test.ts index 1b4a3338ca1..a285af53fb9 100644 --- a/packages/connectors/connector-alipay-web/src/utils.test.ts +++ b/packages/connectors/connector-alipay-web/src/utils.test.ts @@ -2,14 +2,12 @@ import { methodForAccessToken } from './constant.js'; import { mockedAlipayConfigWithValidPrivateKey, mockedAlipayPublicParameters } from './mock.js'; import { signingParameters } from './utils.js'; -const { jest } = import.meta; - -const listenJSONParse = jest.spyOn(JSON, 'parse'); -const listenJSONStringify = jest.spyOn(JSON, 'stringify'); +const listenJSONParse = vi.spyOn(JSON, 'parse'); +const listenJSONStringify = vi.spyOn(JSON, 'stringify'); describe('signingParameters', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); const testingParameters = { diff --git a/packages/connectors/connector-aliyun-dm/CHANGELOG.md b/packages/connectors/connector-aliyun-dm/CHANGELOG.md index c2e3311bcd8..425189c294e 100644 --- a/packages/connectors/connector-aliyun-dm/CHANGELOG.md +++ b/packages/connectors/connector-aliyun-dm/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-aliyun-dm +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-aliyun-dm/package.json b/packages/connectors/connector-aliyun-dm/package.json index 1e8af3bbb8d..1871c95491c 100644 --- a/packages/connectors/connector-aliyun-dm/package.json +++ b/packages/connectors/connector-aliyun-dm/package.json @@ -1,9 +1,9 @@ { "name": "@logto/connector-aliyun-dm", - "version": "1.1.0", + "version": "1.1.1", "description": "Aliyun DM connector implementation.", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -23,9 +23,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": { diff --git a/packages/connectors/connector-aliyun-dm/src/index.test.ts b/packages/connectors/connector-aliyun-dm/src/index.test.ts index 9c8d0ea1a9f..89c6495f5ae 100644 --- a/packages/connectors/connector-aliyun-dm/src/index.test.ts +++ b/packages/connectors/connector-aliyun-dm/src/index.test.ts @@ -2,16 +2,14 @@ import { TemplateType } from '@logto/connector-kit'; import { mockedConfigWithAllRequiredTemplates } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConfigWithAllRequiredTemplates); -const getConfig = jest.fn().mockResolvedValue(mockedConfigWithAllRequiredTemplates); - -const singleSendMail = jest.fn(() => ({ +const singleSendMail = vi.fn(() => ({ body: JSON.stringify({ EnvId: 'env-id', RequestId: 'request-id' }), statusCode: 200, })); -jest.unstable_mockModule('./single-send-mail.js', () => ({ +vi.mock('./single-send-mail.js', () => ({ singleSendMail, })); @@ -19,7 +17,7 @@ const { default: createConnector } = await import('./index.js'); describe('sendMessage()', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should call singleSendMail() with correct template and content', async () => { diff --git a/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts b/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts index 4804ad8e78a..1162b5d3210 100644 --- a/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts +++ b/packages/connectors/connector-aliyun-dm/src/single-send-mail.test.ts @@ -1,8 +1,6 @@ -const { jest } = import.meta; +const request = vi.fn(); -const request = jest.fn(); - -jest.unstable_mockModule('./utils.js', () => ({ +vi.mock('./utils.js', () => ({ request, })); diff --git a/packages/connectors/connector-aliyun-dm/src/utils.test.ts b/packages/connectors/connector-aliyun-dm/src/utils.test.ts index 87d18dfad40..24da02e7a88 100644 --- a/packages/connectors/connector-aliyun-dm/src/utils.test.ts +++ b/packages/connectors/connector-aliyun-dm/src/utils.test.ts @@ -1,10 +1,8 @@ import { mockedParameters } from './mock.js'; -const { jest } = import.meta; +const post = vi.fn(); -const post = jest.fn(); - -jest.unstable_mockModule('got', () => ({ +vi.mock('got', () => ({ got: { post }, })); diff --git a/packages/connectors/connector-aliyun-sms/CHANGELOG.md b/packages/connectors/connector-aliyun-sms/CHANGELOG.md index b2ab91a5a5c..952eb8f9155 100644 --- a/packages/connectors/connector-aliyun-sms/CHANGELOG.md +++ b/packages/connectors/connector-aliyun-sms/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-aliyun-sms +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-aliyun-sms/package.json b/packages/connectors/connector-aliyun-sms/package.json index 82f36fc3ea7..5bd96d213fa 100644 --- a/packages/connectors/connector-aliyun-sms/package.json +++ b/packages/connectors/connector-aliyun-sms/package.json @@ -1,9 +1,9 @@ { "name": "@logto/connector-aliyun-sms", - "version": "1.1.0", + "version": "1.1.1", "description": "Aliyun SMS connector implementation.", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -23,9 +23,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": { diff --git a/packages/connectors/connector-aliyun-sms/src/index.test.ts b/packages/connectors/connector-aliyun-sms/src/index.test.ts index 10dba3ed0d8..bfbacf15b66 100644 --- a/packages/connectors/connector-aliyun-sms/src/index.test.ts +++ b/packages/connectors/connector-aliyun-sms/src/index.test.ts @@ -2,16 +2,14 @@ import { TemplateType } from '@logto/connector-kit'; import { mockedConnectorConfig, phoneTest, codeTest } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConnectorConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConnectorConfig); - -const sendSms = jest.fn().mockResolvedValue({ +const sendSms = vi.fn().mockResolvedValue({ body: JSON.stringify({ Code: 'OK', RequestId: 'request-id', Message: 'OK' }), statusCode: 200, }); -jest.unstable_mockModule('./single-send-text.js', () => ({ +vi.mock('./single-send-text.js', () => ({ sendSms, })); @@ -19,7 +17,7 @@ const { default: createConnector } = await import('./index.js'); describe('sendMessage()', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should call singleSendMail() and replace code in content', async () => { diff --git a/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts b/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts index 64d269e98a8..ec7da921964 100644 --- a/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts +++ b/packages/connectors/connector-aliyun-sms/src/single-send-text.test.ts @@ -1,10 +1,8 @@ import { mockedRandomCode } from './mock.js'; -const { jest } = import.meta; +const request = vi.fn(); -const request = jest.fn(); - -jest.unstable_mockModule('./utils.js', () => ({ request })); +vi.mock('./utils.js', () => ({ request })); const { sendSms } = await import('./single-send-text.js'); diff --git a/packages/connectors/connector-aliyun-sms/src/utils.test.ts b/packages/connectors/connector-aliyun-sms/src/utils.test.ts index 87d18dfad40..24da02e7a88 100644 --- a/packages/connectors/connector-aliyun-sms/src/utils.test.ts +++ b/packages/connectors/connector-aliyun-sms/src/utils.test.ts @@ -1,10 +1,8 @@ import { mockedParameters } from './mock.js'; -const { jest } = import.meta; +const post = vi.fn(); -const post = jest.fn(); - -jest.unstable_mockModule('got', () => ({ +vi.mock('got', () => ({ got: { post }, })); diff --git a/packages/connectors/connector-apple/CHANGELOG.md b/packages/connectors/connector-apple/CHANGELOG.md index 5a016939923..56904be0ab1 100644 --- a/packages/connectors/connector-apple/CHANGELOG.md +++ b/packages/connectors/connector-apple/CHANGELOG.md @@ -1,5 +1,20 @@ # @logto/connector-apple +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + - @logto/shared@3.1.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-apple/package.json b/packages/connectors/connector-apple/package.json index 084b811a137..cb9ae15e3a0 100644 --- a/packages/connectors/connector-apple/package.json +++ b/packages/connectors/connector-apple/package.json @@ -1,9 +1,9 @@ { "name": "@logto/connector-apple", - "version": "1.2.0", + "version": "1.3.0", "description": "Apple web connector implementation.", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "@logto/shared": "workspace:^3.1.0", "jose": "^5.0.0" }, @@ -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": { diff --git a/packages/connectors/connector-apple/src/index.test.ts b/packages/connectors/connector-apple/src/index.test.ts index fdf8c77716b..7671330055d 100644 --- a/packages/connectors/connector-apple/src/index.test.ts +++ b/packages/connectors/connector-apple/src/index.test.ts @@ -2,15 +2,13 @@ import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const jwtVerify = vi.fn(); -const jwtVerify = jest.fn(); - -jest.unstable_mockModule('jose', () => ({ +vi.mock('jose', () => ({ jwtVerify, - createRemoteJWKSet: jest.fn(), + createRemoteJWKSet: vi.fn(), })); const { authorizationEndpoint } = await import('./constant.js'); @@ -18,12 +16,12 @@ const { default: createConnector } = await import('./index.js'); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { const connector = await createConnector({ getConfig }); - const setSession = jest.fn(); + const setSession = vi.fn(); const authorizationUri = await connector.getAuthorizationUri( { state: 'some_state', @@ -50,7 +48,7 @@ describe('getAuthorizationUri', () => { describe('getUserInfo', () => { afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get user info from id token payload', async () => { @@ -59,8 +57,12 @@ describe('getUserInfo', () => { payload: { sub: userId, email: 'foo@bar.com', email_verified: true }, })); const connector = await createConnector({ getConfig }); - const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, jest.fn()); - expect(userInfo).toEqual({ id: userId, email: 'foo@bar.com' }); + const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, vi.fn()); + expect(userInfo).toEqual({ + id: userId, + email: 'foo@bar.com', + rawData: { id_token: 'idToken' }, + }); }); it('should ignore unverified email', async () => { @@ -68,8 +70,8 @@ describe('getUserInfo', () => { payload: { sub: 'userId', email: 'foo@bar.com' }, })); const connector = await createConnector({ getConfig }); - const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, jest.fn()); - expect(userInfo).toEqual({ id: 'userId' }); + const userInfo = await connector.getUserInfo({ id_token: 'idToken' }, vi.fn()); + expect(userInfo).toEqual({ id: 'userId', rawData: { id_token: 'idToken' } }); }); it('should get user info from the `user` field', async () => { @@ -86,15 +88,26 @@ describe('getUserInfo', () => { name: { firstName: 'foo', lastName: 'bar' }, }), }, - jest.fn() + vi.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 () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); @@ -104,7 +117,7 @@ describe('getUserInfo', () => { throw new Error('jwtVerify failed'); }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ id_token: 'id_token' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ id_token: 'id_token' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid) ); }); @@ -114,7 +127,7 @@ describe('getUserInfo', () => { payload: { iat: 123_456 }, })); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ id_token: 'id_token' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ id_token: 'id_token' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialIdTokenInvalid) ); }); diff --git a/packages/connectors/connector-apple/src/index.ts b/packages/connectors/connector-apple/src/index.ts index fa699aab4e3..bbd23d2d699 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-aws-ses/CHANGELOG.md b/packages/connectors/connector-aws-ses/CHANGELOG.md index dd6073d1c8b..eb0337e4ede 100644 --- a/packages/connectors/connector-aws-ses/CHANGELOG.md +++ b/packages/connectors/connector-aws-ses/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-aws-ses +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-aws-ses/package.json b/packages/connectors/connector-aws-ses/package.json index 558304d7b96..bfc993a75d5 100644 --- a/packages/connectors/connector-aws-ses/package.json +++ b/packages/connectors/connector-aws-ses/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-aws-ses", - "version": "1.1.0", + "version": "1.1.1", "description": "Logto Connector for Amazon SES", "author": "Jeff ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "@aws-sdk/client-sesv2": "^3.224.0", "@aws-sdk/types": "^3.226.0" }, @@ -26,9 +26,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": { diff --git a/packages/connectors/connector-aws-ses/src/index.test.ts b/packages/connectors/connector-aws-ses/src/index.test.ts index f95720a70fc..408b6de163d 100644 --- a/packages/connectors/connector-aws-ses/src/index.test.ts +++ b/packages/connectors/connector-aws-ses/src/index.test.ts @@ -4,11 +4,9 @@ import { TemplateType } from '@logto/connector-kit'; import createConnector from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConfig); - -jest.spyOn(SESv2Client.prototype, 'send').mockResolvedValue({ +vi.spyOn(SESv2Client.prototype, 'send').mockResolvedValue({ MessageId: 'mocked-message-id', $metadata: { httpStatusCode: 200, @@ -17,7 +15,7 @@ jest.spyOn(SESv2Client.prototype, 'send').mockResolvedValue({ describe('sendMessage()', () => { afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should call SendMail() with correct template and content', async () => { diff --git a/packages/connectors/connector-azuread/CHANGELOG.md b/packages/connectors/connector-azuread/CHANGELOG.md index 3044ce85d77..f24cfea3574 100644 --- a/packages/connectors/connector-azuread/CHANGELOG.md +++ b/packages/connectors/connector-azuread/CHANGELOG.md @@ -1,5 +1,24 @@ # @logto/connector-azuread +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- 5cde35ec1: Update the Microsoft social connector integration guide. + + - Reorganize the content to make it more readable. + - Exclusively explained the different access types and their corresponding tenant IDs in the Azure Portal. + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-azuread/README.md b/packages/connectors/connector-azuread/README.md index 6578b956eb3..ac09b62863a 100644 --- a/packages/connectors/connector-azuread/README.md +++ b/packages/connectors/connector-azuread/README.md @@ -3,6 +3,7 @@ The Microsoft Azure AD connector provides a succinct way for your application to use Azure’s OAuth 2.0 authentication system. **Table of contents** + - [Microsoft Azure AD connector](#microsoft-azure-ad-connector) - [Set up Microsoft Azure AD in the Azure Portal](#set-up-microsoft-azure-ad-in-the-azure-portal) - [Fill in the configuration](#fill-in-the-configuration) @@ -14,34 +15,42 @@ The Microsoft Azure AD connector provides a succinct way for your application to - Visit the [Azure Portal](https://portal.azure.com/#home) and sign in with your Azure account. You need to have an active subscription to access Microsoft Azure AD. - Click the **Azure Active Directory** from the services they offer, and click the **App Registrations** from the left menu. -- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_endpoint}/callback/${connector_id}`. e.g. `https://foo.logto.app/callback/${connector_id}`. (The `connector_id` can be also found on the top bar of the Logto Admin Console connector details page) -- You need to select Web as Platform. - - If you select **Sign in users of a specific organization only** for access type then you need to enter **TenantID**. - - If you select **Sign in users with work and school accounts or personal Microsoft accounts** for access type then you need to enter **common**. - - If you select **Sign in users with work and school accounts** for access type then you need to enter **organizations**. - - If you select **Sign in users with personal Microsoft accounts (MSA) only** for access type then you need to enter **consumers**. +- Click **New Registration** at the top, enter a description, select your **access type** and add your **Redirect URI**, which will redirect the user to the application after logging in. In our case, this will be `${your_logto_endpoint}/callback/${connector_id}`. e.g. `https://foo.logto.app/callback/${connector_id}`. (The `connector_id` can be also found on the top bar of the Logto Admin Console connector details page) + > You can copy the `Callback URI` in the configuration section. +- Select Web as Platform. -> You can copy the `Callback URI` in the configuration section. +## Fill in the configuration in Logto -## Fill in the configuration +| Name | Type | +| ------------- | ------ | +| clientId | string | +| clientSecret | string | +| tenantId | string | +| cloudInstance | string | -In details page of the newly registered app, you can find the **Application (client) ID** and **Directory (tenant) ID**. +### Client ID -For **Cloud Instance**, usually it is `https://login.microsoftonline.com/`. See [Azure AD authentication endpoints](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints) for more information. +You may find the **Application (client) ID** in the **Overview** section of your newly created application in the Azure Portal. + +### Client Secret -## Configure your client secret - In your newly created application, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top. - Enter a description and an expiration. - This will only show your client secret once. Fill the **value** to the Logto connector configuration and save it to a secure location. -## Config types +### Cloud Instance -| Name | Type | -| ------------- | ------ | -| clientId | string | -| clientSecret | string | -| tenantId | string | -| cloudInstance | string | +Usually, it is `https://login.microsoftonline.com/`. See [Azure AD authentication endpoints](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints) for more information. + +### Tenant ID + +Logto will use this field to construct the authorization endpoints. This value is dependent on the **access type** you selected when creating the application in the Azure Portal. + +- If you select **Accounts in this organizational directory only** for access type then you need to enter your **{TenantID}**. You can find the tenant ID in the **Overview** section of your Azure Active Directory. +- If you select **Accounts in any organizational directory** for access type then you need to enter **organizations**. +- If you select **Accounts in any organizational directory or personal Microsoft accounts** for access type then you need to enter **common**. +- If you select **Personal Microsoft accounts only** for access type then you need to enter **consumers**. ## References -* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview) + +- [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview) diff --git a/packages/connectors/connector-azuread/package.json b/packages/connectors/connector-azuread/package.json index fdb999b7561..236885e7cc9 100644 --- a/packages/connectors/connector-azuread/package.json +++ b/packages/connectors/connector-azuread/package.json @@ -1,11 +1,11 @@ { "name": "@logto/connector-azuread", - "version": "1.1.0", + "version": "1.2.0", "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:^3.0.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 0be0dd8f691..1368f85b64b 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 91c95332da8..eec02121a34 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/CHANGELOG.md b/packages/connectors/connector-discord/CHANGELOG.md index 6fa918aead1..7a1db6bf874 100644 --- a/packages/connectors/connector-discord/CHANGELOG.md +++ b/packages/connectors/connector-discord/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-discord +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-discord/package.json b/packages/connectors/connector-discord/package.json index 3cfe00c2916..c1c148df7f5 100644 --- a/packages/connectors/connector-discord/package.json +++ b/packages/connectors/connector-discord/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-discord", - "version": "1.2.0", + "version": "1.3.0", "description": "Discord connector implementation.", "author": "ZR3SYSTEMS. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-discord/src/index.test.ts b/packages/connectors/connector-discord/src/index.test.ts index 31a20641a07..45664a11d50 100644 --- a/packages/connectors/connector-discord/src/index.test.ts +++ b/packages/connectors/connector-discord/src/index.test.ts @@ -6,14 +6,12 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('Discord connector', () => { describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid authorizationUri with redirectUri and state', async () => { @@ -27,7 +25,7 @@ describe('Discord connector', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=identify+email&state=some_state` @@ -38,7 +36,7 @@ describe('Discord connector', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -66,7 +64,7 @@ describe('Discord connector', () => { await expect( getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' }) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); }); @@ -82,7 +80,7 @@ describe('Discord connector', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid SocialUserInfo', async () => { @@ -99,13 +97,20 @@ describe('Discord connector', () => { code: 'code', redirectUri: 'dummyRedirectUri', }, - jest.fn() + vi.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, + }, }); }); @@ -113,15 +118,15 @@ describe('Discord connector', () => { nock(userInfoEndpoint).get('').reply(401); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, vi.fn()) + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); }); it('throws unrecognized error', async () => { nock(userInfoEndpoint).get('').reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, jest.fn()) + connector.getUserInfo({ code: 'code', redirectUri: 'dummyRedirectUri' }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-discord/src/index.ts b/packages/connectors/connector-discord/src/index.ts index 224543952e3..4483cc2e9a9 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/CHANGELOG.md b/packages/connectors/connector-facebook/CHANGELOG.md index 0114694dcb8..9e5e7bcf628 100644 --- a/packages/connectors/connector-facebook/CHANGELOG.md +++ b/packages/connectors/connector-facebook/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-facebook +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-facebook/package.json b/packages/connectors/connector-facebook/package.json index ad43c9f85d9..ec63896d6a5 100644 --- a/packages/connectors/connector-facebook/package.json +++ b/packages/connectors/connector-facebook/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-facebook", - "version": "1.2.0", + "version": "1.3.0", "description": "Facebook web connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-facebook/src/index.test.ts b/packages/connectors/connector-facebook/src/index.test.ts index d0919328928..69df5e0d076 100644 --- a/packages/connectors/connector-facebook/src/index.test.ts +++ b/packages/connectors/connector-facebook/src/index.test.ts @@ -6,14 +6,12 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { clientId, clientSecret, code, dummyRedirectUri, fields, mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('Facebook connector', () => { describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid authorizationUri with redirectUri and state', async () => { @@ -29,7 +27,7 @@ describe('Facebook connector', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); const encodedRedirectUri = encodeURIComponent(redirectUri); @@ -42,7 +40,7 @@ describe('Facebook connector', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -86,7 +84,7 @@ describe('Facebook connector', () => { await expect( getAccessToken(mockedConfig, { code, redirectUri: dummyRedirectUri }) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); }); @@ -110,7 +108,7 @@ describe('Facebook connector', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid SocialUserInfo', async () => { @@ -130,13 +128,19 @@ describe('Facebook connector', () => { code, redirectUri: dummyRedirectUri, }, - jest.fn() + vi.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 } }, + }, }); }); @@ -144,8 +148,8 @@ describe('Facebook connector', () => { nock(userInfoEndpoint).get('').query({ fields }).reply(400); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, vi.fn()) + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); }); it('throws AuthorizationFailed error if error is access_denied', async () => { @@ -168,9 +172,9 @@ describe('Facebook connector', () => { error_description: 'Permissions error.', error_reason: 'user_denied', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, 'Permissions error.') ); }); @@ -195,9 +199,9 @@ describe('Facebook connector', () => { error_description: 'General error encountered.', error_reason: 'user_denied', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { error: 'general_error', error_code: 200, @@ -211,7 +215,7 @@ describe('Facebook connector', () => { nock(userInfoEndpoint).get('').reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, jest.fn()) + connector.getUserInfo({ code, redirectUri: dummyRedirectUri }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-facebook/src/index.ts b/packages/connectors/connector-facebook/src/index.ts index 7cb622c0ff0..0aa677a247a 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/CHANGELOG.md b/packages/connectors/connector-feishu-web/CHANGELOG.md index c83ecc53428..69fca21a6ae 100644 --- a/packages/connectors/connector-feishu-web/CHANGELOG.md +++ b/packages/connectors/connector-feishu-web/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-feishu-web +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-feishu-web/package.json b/packages/connectors/connector-feishu-web/package.json index 1d552351d1b..7fc99edaaef 100644 --- a/packages/connectors/connector-feishu-web/package.json +++ b/packages/connectors/connector-feishu-web/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-feishu-web", - "version": "1.1.0", + "version": "1.2.0", "description": "Feishu web connector.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-feishu-web/src/index.test.ts b/packages/connectors/connector-feishu-web/src/index.test.ts index dc9871c97b7..7a20668fc71 100644 --- a/packages/connectors/connector-feishu-web/src/index.test.ts +++ b/packages/connectors/connector-feishu-web/src/index.test.ts @@ -6,13 +6,11 @@ import { accessTokenEndpoint, codeEndpoint, userInfoEndpoint } from './constant. import createConnector, { buildAuthorizationUri, getAccessToken } from './index.js'; import { mockedFeishuConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedFeishuConfig); +const getConfig = vi.fn().mockResolvedValue(mockedFeishuConfig); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should build authorization uri', function () { @@ -33,7 +31,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${codeEndpoint}?client_id=1112233&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&response_type=code&state=some_state` @@ -44,7 +42,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const accessTokenUrl = new URL(accessTokenEndpoint); @@ -73,7 +71,7 @@ describe('getAccessToken', () => { await expect( getAccessToken('code', '123', '123', 'http://localhost:3000') - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'access_token is empty') ); }); @@ -86,7 +84,7 @@ describe('getAccessToken', () => { await expect( getAccessToken('code', '123', '123', 'http://localhost:3000') - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'invalid code') ); }); @@ -95,7 +93,7 @@ describe('getAccessToken', () => { describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); beforeEach(() => { @@ -112,7 +110,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,23 +127,25 @@ 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', }, - jest.fn() + vi.fn() ); 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 () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}') ); }); @@ -157,8 +157,8 @@ describe('getUserInfo', () => { }); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'error_code', redirectUri: 'http://localhost:3000' }, jest.fn()) - ).rejects.toMatchError( + connector.getUserInfo({ code: 'error_code', redirectUri: 'http://localhost:3000' }, vi.fn()) + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'invalid access token') ); }); @@ -169,8 +169,8 @@ describe('getUserInfo', () => { }); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: 'http://localhost:3000' }, jest.fn()) - ).rejects.toMatchError( + connector.getUserInfo({ code: 'code', redirectUri: 'http://localhost:3000' }, vi.fn()) + ).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.InvalidResponse, 'invalid user response') ); }); @@ -179,7 +179,7 @@ describe('getUserInfo', () => { nock(userInfoUrl.origin).get(userInfoUrl.pathname).query(true).reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: 'http://localhost:3000' }, jest.fn()) + connector.getUserInfo({ code: 'code', redirectUri: 'http://localhost:3000' }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-feishu-web/src/index.ts b/packages/connectors/connector-feishu-web/src/index.ts index d5c8f36f7f9..a2b25b2f929 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/CHANGELOG.md b/packages/connectors/connector-github/CHANGELOG.md index ca98e8abd7a..73b8a2eb0ee 100644 --- a/packages/connectors/connector-github/CHANGELOG.md +++ b/packages/connectors/connector-github/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-github +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-github/package.json b/packages/connectors/connector-github/package.json index 63295f5c46a..30bd1beaee2 100644 --- a/packages/connectors/connector-github/package.json +++ b/packages/connectors/connector-github/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-github", - "version": "1.2.0", + "version": "1.3.0", "description": "Github web connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "query-string": "^9.0.0" }, "main": "./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": { diff --git a/packages/connectors/connector-github/src/index.test.ts b/packages/connectors/connector-github/src/index.test.ts index 6a5df4cf557..0656245e073 100644 --- a/packages/connectors/connector-github/src/index.test.ts +++ b/packages/connectors/connector-github/src/index.test.ts @@ -7,13 +7,11 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { @@ -27,7 +25,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&state=some_state&scope=read%3Auser` @@ -38,7 +36,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -60,7 +58,7 @@ describe('getAccessToken', () => { nock(accessTokenEndpoint) .post('') .reply(200, qs.stringify({ access_token: '', scope: 'scope', token_type: 'token_type' })); - await expect(getAccessToken(mockedConfig, { code: 'code' })).rejects.toMatchError( + await expect(getAccessToken(mockedConfig, { code: 'code' })).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid) ); }); @@ -82,7 +80,7 @@ describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid SocialUserInfo', async () => { @@ -91,14 +89,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({ + const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn()); + 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', + }, }); }); @@ -110,16 +116,22 @@ describe('getUserInfo', () => { email: null, }); const connector = await createConnector({ getConfig }); - const socialUserInfo = await connector.getUserInfo({ code: 'code' }, jest.fn()); + const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.fn()); expect(socialUserInfo).toMatchObject({ id: '1', + rawData: { + id: 1, + avatar_url: null, + name: null, + email: null, + }, }); }); it('throws SocialAccessTokenInvalid error if remote response code is 401', async () => { nock(userInfoEndpoint).get('').reply(401); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) ); }); @@ -140,9 +152,9 @@ describe('getUserInfo', () => { error_uri: 'https://docs.github.com/apps/troubleshooting-authorization-request-errors#access-denied', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError( ConnectorErrorCodes.AuthorizationFailed, 'The user has denied your application access.' @@ -164,9 +176,9 @@ describe('getUserInfo', () => { error: 'general_error', error_description: 'General error encountered.', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError( ConnectorErrorCodes.General, '{"error":"general_error","error_description":"General error encountered."}' @@ -177,6 +189,6 @@ describe('getUserInfo', () => { it('throws unrecognized error', async () => { nock(userInfoEndpoint).get('').reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-github/src/index.ts b/packages/connectors/connector-github/src/index.ts index 547ce396452..52a0e97f182 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/CHANGELOG.md b/packages/connectors/connector-google/CHANGELOG.md index 2a7f2b638f1..2f79c263150 100644 --- a/packages/connectors/connector-google/CHANGELOG.md +++ b/packages/connectors/connector-google/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-google +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-google/package.json b/packages/connectors/connector-google/package.json index 2148928fb6c..cb5dee0d232 100644 --- a/packages/connectors/connector-google/package.json +++ b/packages/connectors/connector-google/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-google", - "version": "1.2.0", + "version": "1.3.0", "description": "Google web connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-google/src/index.test.ts b/packages/connectors/connector-google/src/index.test.ts index 21560f0ad8d..4b258f02144 100644 --- a/packages/connectors/connector-google/src/index.test.ts +++ b/packages/connectors/connector-google/src/index.test.ts @@ -6,14 +6,12 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('google connector', () => { describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid authorizationUri with redirectUri and state', async () => { @@ -27,7 +25,7 @@ describe('google connector', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=some_state&scope=openid+profile+email` @@ -38,7 +36,7 @@ describe('google connector', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -60,7 +58,7 @@ describe('google connector', () => { .reply(200, { access_token: '', scope: 'scope', token_type: 'token_type' }); await expect( getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' }) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); }); @@ -75,11 +73,11 @@ describe('google connector', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpoint).post('').reply(200, { + const jsonResponse = Object.freeze({ sub: '1234567890', name: 'monalisa octocat', given_name: 'monalisa', @@ -89,19 +87,21 @@ describe('google connector', () => { email_verified: true, locale: 'en', }); + nock(userInfoEndpoint).post('').reply(200, jsonResponse); const connector = await createConnector({ getConfig }); const socialUserInfo = await connector.getUserInfo( { code: 'code', redirectUri: 'redirectUri', }, - jest.fn() + vi.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, }); }); @@ -109,8 +109,8 @@ describe('google connector', () => { nock(userInfoEndpoint).post('').reply(401); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); }); it('throws General error', async () => { @@ -131,9 +131,9 @@ describe('google connector', () => { error: 'general_error', error_description: 'General error encountered.', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError( ConnectorErrorCodes.General, '{"error":"general_error","error_description":"General error encountered."}' @@ -145,7 +145,7 @@ describe('google connector', () => { nock(userInfoEndpoint).post('').reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-google/src/index.ts b/packages/connectors/connector-google/src/index.ts index db6a6ad0990..19c8fd9a273 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/CHANGELOG.md b/packages/connectors/connector-kakao/CHANGELOG.md index 786b8f9b1e1..341fe977229 100644 --- a/packages/connectors/connector-kakao/CHANGELOG.md +++ b/packages/connectors/connector-kakao/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-kakao +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-kakao/package.json b/packages/connectors/connector-kakao/package.json index 6da7db82f49..b89bdb04999 100644 --- a/packages/connectors/connector-kakao/package.json +++ b/packages/connectors/connector-kakao/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-kakao", - "version": "1.1.0", + "version": "1.2.0", "description": "Kakao connector implementation.", "author": "Kyungyoon Kim. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-kakao/src/index.test.ts b/packages/connectors/connector-kakao/src/index.test.ts index 90db9c0cd8f..830897ebd5a 100644 --- a/packages/connectors/connector-kakao/src/index.test.ts +++ b/packages/connectors/connector-kakao/src/index.test.ts @@ -6,14 +6,12 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('kakao connector', () => { describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid authorizationUri with redirectUri and state', async () => { @@ -27,7 +25,7 @@ describe('kakao connector', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=some_state` @@ -38,7 +36,7 @@ describe('kakao connector', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -60,7 +58,7 @@ describe('kakao connector', () => { .reply(200, { access_token: '', scope: 'scope', token_type: 'token_type' }); await expect( getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' }) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); }); @@ -75,37 +73,37 @@ describe('kakao connector', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid SocialUserInfo', async () => { - nock(userInfoEndpoint) - .post('') - .reply(200, { - id: 1_234_567_890, - kakao_account: { - is_email_valid: true, - email: 'ruddbs5302@gmail.com', - profile: { - nickname: 'pemassi', - profile_image_url: 'https://github.com/images/error/octocat_happy.gif', - is_default_image: false, - }, + const 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( { code: 'code', redirectUri: 'redirectUri', }, - jest.fn() + vi.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, }); }); @@ -113,8 +111,8 @@ describe('kakao connector', () => { nock(userInfoEndpoint).post('').reply(401); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); }); it('throws General error', async () => { @@ -135,9 +133,9 @@ describe('kakao connector', () => { error: 'general_error', error_description: 'General error encountered.', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError( ConnectorErrorCodes.General, '{"error":"general_error","error_description":"General error encountered."}' @@ -149,7 +147,7 @@ describe('kakao connector', () => { nock(userInfoEndpoint).post('').reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-kakao/src/index.ts b/packages/connectors/connector-kakao/src/index.ts index 1c8dbf72620..a07eadb8d0c 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-logto-email/CHANGELOG.md b/packages/connectors/connector-logto-email/CHANGELOG.md index b859f7ceb51..0742b6e2d83 100644 --- a/packages/connectors/connector-logto-email/CHANGELOG.md +++ b/packages/connectors/connector-logto-email/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-logto-email +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-logto-email/package.json b/packages/connectors/connector-logto-email/package.json index db0a66b2fc9..fb5f6d8a668 100644 --- a/packages/connectors/connector-logto-email/package.json +++ b/packages/connectors/connector-logto-email/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-logto-email", - "version": "1.1.0", + "version": "1.1.1", "description": "Logto email connector.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { @@ -49,6 +48,6 @@ "access": "public" }, "devDependencies": { - "@logto/cloud": "0.2.5-faca9a9" + "@logto/cloud": "0.2.5-ab8a489" } } diff --git a/packages/connectors/connector-logto-email/src/index.test.ts b/packages/connectors/connector-logto-email/src/index.test.ts index e38af0d89ec..084e461100d 100644 --- a/packages/connectors/connector-logto-email/src/index.test.ts +++ b/packages/connectors/connector-logto-email/src/index.test.ts @@ -6,16 +6,14 @@ import { TemplateType } from '@logto/connector-kit'; import { emailEndpoint, usageEndpoint } from './constant.js'; import createConnector from './index.js'; -const { jest } = import.meta; - const endpoint = 'http://localhost:3003'; const api = got.extend({ prefixUrl: endpoint }); const dropLeadingSlash = (path: string) => path.replace(/^\//, ''); const buildUrl = (path: string, endpoint: string) => new URL(`${endpoint}/api${path}`); -const getConfig = jest.fn().mockResolvedValue({}); -const getCloudServiceClient = jest.fn().mockResolvedValue({ +const getConfig = vi.fn().mockResolvedValue({}); +const getCloudServiceClient = vi.fn().mockResolvedValue({ post: async (path: string, payload: { body: unknown }) => { return api(dropLeadingSlash(path), { method: 'POST', diff --git a/packages/connectors/connector-logto-email/src/index.ts b/packages/connectors/connector-logto-email/src/index.ts index 1a7451bd622..2e64e7f2385 100644 --- a/packages/connectors/connector-logto-email/src/index.ts +++ b/packages/connectors/connector-logto-email/src/index.ts @@ -38,7 +38,16 @@ const sendMessage = try { await client.post(`/api${emailEndpoint}`, { body: { - data: { to, type, payload: { ...payload, senderName, companyInformation, appLogo } }, + data: { + to, + type, + payload: { + ...payload, + ...conditional(senderName && { senderName }), + ...conditional(companyInformation && { companyInformation }), + ...conditional(appLogo && { appLogo }), + }, + }, }, }); } catch (error: unknown) { diff --git a/packages/connectors/connector-logto-sms/CHANGELOG.md b/packages/connectors/connector-logto-sms/CHANGELOG.md index 1265cc0cb1c..cdb1dcb483d 100644 --- a/packages/connectors/connector-logto-sms/CHANGELOG.md +++ b/packages/connectors/connector-logto-sms/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-logto-sms +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-logto-sms/package.json b/packages/connectors/connector-logto-sms/package.json index 6113e8b022a..2a81c137182 100644 --- a/packages/connectors/connector-logto-sms/package.json +++ b/packages/connectors/connector-logto-sms/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-logto-sms", - "version": "1.1.0", + "version": "1.1.1", "description": "Logto SMS connector.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-logto-sms/src/index.test.ts b/packages/connectors/connector-logto-sms/src/index.test.ts index 740d7b34fd1..95d1ed10ee5 100644 --- a/packages/connectors/connector-logto-sms/src/index.test.ts +++ b/packages/connectors/connector-logto-sms/src/index.test.ts @@ -5,9 +5,7 @@ import { TemplateType } from '@logto/connector-kit'; import { smsEndpoint } from './constant.js'; import { mockedAccessTokenResponse, mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); const { default: createConnector } = await import('./index.js'); diff --git a/packages/connectors/connector-logto-social-demo/CHANGELOG.md b/packages/connectors/connector-logto-social-demo/CHANGELOG.md index 20666c2e121..ba7962448b0 100644 --- a/packages/connectors/connector-logto-social-demo/CHANGELOG.md +++ b/packages/connectors/connector-logto-social-demo/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-logto-social-demo +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-logto-social-demo/package.json b/packages/connectors/connector-logto-social-demo/package.json index 8fdd8495858..0a12e00a09b 100644 --- a/packages/connectors/connector-logto-social-demo/package.json +++ b/packages/connectors/connector-logto-social-demo/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-logto-social-demo", - "version": "1.1.0", + "version": "1.1.1", "description": "OAuth standard connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-logto-social-demo/src/index.test.ts b/packages/connectors/connector-logto-social-demo/src/index.test.ts index 63aaf3990e4..0bce8984e6c 100644 --- a/packages/connectors/connector-logto-social-demo/src/index.test.ts +++ b/packages/connectors/connector-logto-social-demo/src/index.test.ts @@ -2,14 +2,12 @@ import createConnector from './index.js'; import type { SocialDemoConfig } from './types.js'; import { SocialProvider } from './types.js'; -const { jest } = import.meta; - const mockedConfig: SocialDemoConfig = { provider: SocialProvider.GitHub, clientId: 'client-id', redirectUri: 'http://localhost:3000/callback', }; -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { it('should get a valid uri by redirectUri and state', async () => { @@ -23,7 +21,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toContain(encodeURIComponent(mockedConfig.redirectUri)); expect(authorizationUri).toContain( diff --git a/packages/connectors/connector-mailgun/CHANGELOG.md b/packages/connectors/connector-mailgun/CHANGELOG.md index 61946f1a55b..2820a1bb633 100644 --- a/packages/connectors/connector-mailgun/CHANGELOG.md +++ b/packages/connectors/connector-mailgun/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-mailgun +## 1.2.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-mailgun/package.json b/packages/connectors/connector-mailgun/package.json index af66a5b2366..9345a26c89e 100644 --- a/packages/connectors/connector-mailgun/package.json +++ b/packages/connectors/connector-mailgun/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-mailgun", - "version": "1.2.0", + "version": "1.2.1", "description": "Mailgun connector for Logto.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-mailgun/src/index.test.ts b/packages/connectors/connector-mailgun/src/index.test.ts index 50468ce537c..ac1b4d03456 100644 --- a/packages/connectors/connector-mailgun/src/index.test.ts +++ b/packages/connectors/connector-mailgun/src/index.test.ts @@ -5,9 +5,7 @@ import { TemplateType } from '@logto/connector-kit'; import createMailgunConnector from './index.js'; import { type MailgunConfig } from './types.js'; -const { jest } = import.meta; - -const getConfig = jest.fn(); +const getConfig = vi.fn(); const domain = 'example.com'; const apiKey = 'apiKey'; @@ -210,7 +208,7 @@ describe('Maligun connector', () => { code: '123456', }, }) - ).rejects.toThrowErrorMatchingInlineSnapshot('"ConnectorError: template_not_found"'); + ).rejects.toThrowErrorMatchingInlineSnapshot('[Error: ConnectorError: template_not_found]'); await expect( connector.sendMessage({ @@ -221,7 +219,7 @@ describe('Maligun connector', () => { code: '123456', }, }) - ).rejects.toThrowErrorMatchingInlineSnapshot('"ConnectorError: template_not_found"'); + ).rejects.toThrowErrorMatchingInlineSnapshot('[Error: ConnectorError: template_not_found]'); }); it('should throw error if mailgun returns error', async () => { @@ -247,7 +245,7 @@ describe('Maligun connector', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - '"ConnectorError: {"statusCode":400,"body":"{\\"message\\":\\"error\\"}"}"' + '[Error: ConnectorError: {"statusCode":400,"body":"{\\"message\\":\\"error\\"}"}]' ); }); }); diff --git a/packages/connectors/connector-mock-email-alternative/CHANGELOG.md b/packages/connectors/connector-mock-email-alternative/CHANGELOG.md index cda777eea55..a1142990d04 100644 --- a/packages/connectors/connector-mock-email-alternative/CHANGELOG.md +++ b/packages/connectors/connector-mock-email-alternative/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-mock-standard-email +## 2.0.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 2.0.0 ### Major Changes diff --git a/packages/connectors/connector-mock-email-alternative/package.json b/packages/connectors/connector-mock-email-alternative/package.json index 63fa84fd9a9..b44be9b87ea 100644 --- a/packages/connectors/connector-mock-email-alternative/package.json +++ b/packages/connectors/connector-mock-email-alternative/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-mock-standard-email", - "version": "2.0.0", + "version": "2.0.1", "description": "Mock Standard Email Service connector implementation for integration tests only.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "scripts": { "precommit": "lint-staged", @@ -13,9 +13,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" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mock-email/CHANGELOG.md b/packages/connectors/connector-mock-email/CHANGELOG.md index bea9665a940..4ad35a91007 100644 --- a/packages/connectors/connector-mock-email/CHANGELOG.md +++ b/packages/connectors/connector-mock-email/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-mock-email +## 2.0.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 2.0.0 ### Major Changes diff --git a/packages/connectors/connector-mock-email/package.json b/packages/connectors/connector-mock-email/package.json index 96e5f94c2d0..937c34f1580 100644 --- a/packages/connectors/connector-mock-email/package.json +++ b/packages/connectors/connector-mock-email/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-mock-email", - "version": "2.0.0", + "version": "2.0.1", "description": "Mock Email Service connector implementation for integration tests only.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "scripts": { "precommit": "lint-staged", @@ -13,9 +13,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" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mock-sms/CHANGELOG.md b/packages/connectors/connector-mock-sms/CHANGELOG.md index 59b928d4db8..a72ac350823 100644 --- a/packages/connectors/connector-mock-sms/CHANGELOG.md +++ b/packages/connectors/connector-mock-sms/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-mock-sms +## 2.0.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 2.0.0 ### Major Changes diff --git a/packages/connectors/connector-mock-sms/package.json b/packages/connectors/connector-mock-sms/package.json index 9678d8e704e..46a0a74bb2d 100644 --- a/packages/connectors/connector-mock-sms/package.json +++ b/packages/connectors/connector-mock-sms/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-mock-sms", - "version": "2.0.0", + "version": "2.0.1", "description": "Mock SMS connector implementation for integration tests only.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "scripts": { "precommit": "lint-staged", @@ -13,9 +13,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" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mock-social/CHANGELOG.md b/packages/connectors/connector-mock-social/CHANGELOG.md index 202d3db7f54..57dcf6d62ec 100644 --- a/packages/connectors/connector-mock-social/CHANGELOG.md +++ b/packages/connectors/connector-mock-social/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-mock-social +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-mock-social/package.json b/packages/connectors/connector-mock-social/package.json index 1b49bbaf661..4726bd25e47 100644 --- a/packages/connectors/connector-mock-social/package.json +++ b/packages/connectors/connector-mock-social/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-mock-social", - "version": "1.1.0", + "version": "1.2.0", "description": "Social mock connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "scripts": { "precommit": "lint-staged", @@ -13,9 +13,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" }, "main": "./lib/index.js", diff --git a/packages/connectors/connector-mock-social/src/index.ts b/packages/connectors/connector-mock-social/src/index.ts index 2b6d0d4663e..b9c76058c35 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/CHANGELOG.md b/packages/connectors/connector-naver/CHANGELOG.md index 90ae66b8ab8..978f2a4c883 100644 --- a/packages/connectors/connector-naver/CHANGELOG.md +++ b/packages/connectors/connector-naver/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-naver +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-naver/package.json b/packages/connectors/connector-naver/package.json index 66154e9c50c..09fa173db46 100644 --- a/packages/connectors/connector-naver/package.json +++ b/packages/connectors/connector-naver/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-naver", - "version": "1.1.0", + "version": "1.2.0", "description": "Naver connector implementation.", "author": "Kyungyoon Kim. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-naver/src/index.test.ts b/packages/connectors/connector-naver/src/index.test.ts index e4191463768..03268914c75 100644 --- a/packages/connectors/connector-naver/src/index.test.ts +++ b/packages/connectors/connector-naver/src/index.test.ts @@ -6,14 +6,12 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('naver connector', () => { describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid authorizationUri with redirectUri and state', async () => { @@ -27,7 +25,7 @@ describe('naver connector', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?client_id=%3Cclient-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=some_state` @@ -38,7 +36,7 @@ describe('naver connector', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get an accessToken by exchanging with code', async () => { @@ -60,7 +58,7 @@ describe('naver connector', () => { .reply(200, { access_token: '', scope: 'scope', token_type: 'token_type' }); await expect( getAccessToken(mockedConfig, { code: 'code', redirectUri: 'dummyRedirectUri' }) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid)); }); }); @@ -75,39 +73,39 @@ describe('naver connector', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); 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( { code: 'code', redirectUri: 'redirectUri', }, - jest.fn() + vi.fn() ); expect(socialUserInfo).toMatchObject({ id: '32742776', avatar: 'https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif', name: 'OpenAPI', email: 'openapi@naver.com', + rawData: jsonResponse, }); }); @@ -115,8 +113,8 @@ describe('naver connector', () => { nock(userInfoEndpoint).post('').reply(401); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) - ).rejects.toMatchError(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) + ).rejects.toStrictEqual(new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)); }); it('throws General error', async () => { @@ -137,9 +135,9 @@ describe('naver connector', () => { error: 'general_error', error_description: 'General error encountered.', }, - jest.fn() + vi.fn() ) - ).rejects.toMatchError( + ).rejects.toStrictEqual( new ConnectorError( ConnectorErrorCodes.General, '{"error":"general_error","error_description":"General error encountered."}' @@ -151,7 +149,7 @@ describe('naver connector', () => { nock(userInfoEndpoint).post('').reply(500); const connector = await createConnector({ getConfig }); await expect( - connector.getUserInfo({ code: 'code', redirectUri: '' }, jest.fn()) + connector.getUserInfo({ code: 'code', redirectUri: '' }, vi.fn()) ).rejects.toThrow(); }); }); diff --git a/packages/connectors/connector-naver/src/index.ts b/packages/connectors/connector-naver/src/index.ts index 0a85a9c312b..3e553e4bd88 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/CHANGELOG.md b/packages/connectors/connector-oauth2/CHANGELOG.md index 84ea166ddcb..b8f64dd545f 100644 --- a/packages/connectors/connector-oauth2/CHANGELOG.md +++ b/packages/connectors/connector-oauth2/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-oauth +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-oauth2/package.json b/packages/connectors/connector-oauth2/package.json index eaa3cec19a8..7b73b7e8b90 100644 --- a/packages/connectors/connector-oauth2/package.json +++ b/packages/connectors/connector-oauth2/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-oauth", - "version": "1.1.0", + "version": "1.2.0", "description": "OAuth standard connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "query-string": "^9.0.0" }, "main": "./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": { diff --git a/packages/connectors/connector-oauth2/src/index.test.ts b/packages/connectors/connector-oauth2/src/index.test.ts index d99681876cb..e242fe579f0 100644 --- a/packages/connectors/connector-oauth2/src/index.test.ts +++ b/packages/connectors/connector-oauth2/src/index.test.ts @@ -2,20 +2,18 @@ import nock from 'nock'; import { mockConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockConfig); +const getConfig = vi.fn().mockResolvedValue(mockConfig); const { default: createConnector } = await import('./index.js'); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { const connector = await createConnector({ getConfig }); - const setSession = jest.fn(); + const setSession = vi.fn(); const authorizationUri = await connector.getAuthorizationUri( { state: 'some_state', @@ -40,7 +38,7 @@ describe('getAuthorizationUri', () => { describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid userInfo', async () => { @@ -59,14 +57,15 @@ 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( { code: 'code' }, - jest.fn().mockImplementationOnce(() => { + vi.fn().mockImplementationOnce(() => { 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 4d952dc58b1..f2b1c274b1c 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/CHANGELOG.md b/packages/connectors/connector-oidc/CHANGELOG.md index 31c4366b148..028aa310b9a 100644 --- a/packages/connectors/connector-oidc/CHANGELOG.md +++ b/packages/connectors/connector-oidc/CHANGELOG.md @@ -1,5 +1,20 @@ # @logto/connector-oidc +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + - @logto/shared@3.1.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-oidc/package.json b/packages/connectors/connector-oidc/package.json index f84a7b44c99..bd8871732c9 100644 --- a/packages/connectors/connector-oidc/package.json +++ b/packages/connectors/connector-oidc/package.json @@ -1,9 +1,9 @@ { "name": "@logto/connector-oidc", - "version": "1.1.0", + "version": "1.2.0", "description": "OIDC standard connector implementation.", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "@logto/shared": "workspace:^3.1.0", "jose": "^5.0.0", "nanoid": "^5.0.1" @@ -26,9 +26,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": { diff --git a/packages/connectors/connector-oidc/src/index.test.ts b/packages/connectors/connector-oidc/src/index.test.ts index be4033678a2..b54bc311fdd 100644 --- a/packages/connectors/connector-oidc/src/index.test.ts +++ b/packages/connectors/connector-oidc/src/index.test.ts @@ -2,22 +2,20 @@ import nock from 'nock'; import { mockConfig } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockConfig); -const getConfig = jest.fn().mockResolvedValue(mockConfig); +const jwtVerify = vi.fn(); -const jwtVerify = jest.fn(); - -jest.unstable_mockModule('jose', () => ({ +vi.mock('jose', () => ({ jwtVerify, - createRemoteJWKSet: jest.fn(), + createRemoteJWKSet: vi.fn(), })); const { default: createConnector } = await import('./index.js'); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { @@ -31,7 +29,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); const { origin, pathname, searchParams } = new URL(authorizationUri); @@ -48,7 +46,7 @@ describe('getAuthorizationUri', () => { describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get valid userInfo', async () => { @@ -65,10 +63,10 @@ describe('getUserInfo', () => { const connector = await createConnector({ getConfig }); const userInfo = await connector.getUserInfo( { code: 'code' }, - jest.fn().mockImplementationOnce(() => { + vi.fn().mockImplementationOnce(() => { 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 ae8e6d749dd..a413a26c06f 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-saml/CHANGELOG.md b/packages/connectors/connector-saml/CHANGELOG.md index 6549a3914db..3abfe03a9d0 100644 --- a/packages/connectors/connector-saml/CHANGELOG.md +++ b/packages/connectors/connector-saml/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-saml +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-saml/package.json b/packages/connectors/connector-saml/package.json index aa92eec856b..76dbad1e125 100644 --- a/packages/connectors/connector-saml/package.json +++ b/packages/connectors/connector-saml/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-saml", - "version": "1.1.0", + "version": "1.1.1", "description": "SAML standard connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "fast-xml-parser": "^4.2.5", "samlify": "2.8.10" }, @@ -26,9 +26,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": { diff --git a/packages/connectors/connector-saml/src/index.test.ts b/packages/connectors/connector-saml/src/index.test.ts index 1f564dd2d90..289af187860 100644 --- a/packages/connectors/connector-saml/src/index.test.ts +++ b/packages/connectors/connector-saml/src/index.test.ts @@ -1,18 +1,16 @@ import createConnector, { validateSamlAssertion } from './index.js'; import { mockAttributes, mockedConfig, mockSamlResponse } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConfig); - -const setSession = jest.fn(); -const getSession = jest.fn(); +const setSession = vi.fn(); +const getSession = vi.fn(); const connector = await createConnector({ getConfig }); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri and save required information to storage', async () => { @@ -39,11 +37,11 @@ describe('getAuthorizationUri', () => { describe('validateSamlAssertion', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('Should return right redirectUri', async () => { - jest.useFakeTimers().setSystemTime(new Date('2023-01-18T14:55:45.406Z')); + vi.useFakeTimers().setSystemTime(new Date('2023-01-18T14:55:45.406Z')); getSession.mockResolvedValue({ connectorFactoryId: 'saml', state: 'some_state', @@ -61,13 +59,13 @@ describe('validateSamlAssertion', () => { ); expect(setSession).toHaveBeenCalledWith(expect.anything()); expect(redirectUri).toEqual('http://localhost:3000/callback?state=some_state'); - jest.useRealTimers(); + vi.useRealTimers(); }); }); describe('getUserInfo', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('get right profile', async () => { diff --git a/packages/connectors/connector-saml/src/utils.test.ts b/packages/connectors/connector-saml/src/utils.test.ts index 828471f1e86..5b9ef7a7363 100644 --- a/packages/connectors/connector-saml/src/utils.test.ts +++ b/packages/connectors/connector-saml/src/utils.test.ts @@ -1,10 +1,8 @@ import { userProfileMapping } from './utils.js'; -const { jest } = import.meta; - describe('userProfileMapping', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should return right user profile', () => { diff --git a/packages/connectors/connector-sendgrid-email/CHANGELOG.md b/packages/connectors/connector-sendgrid-email/CHANGELOG.md index 1c1de07b679..a6c4d6730db 100644 --- a/packages/connectors/connector-sendgrid-email/CHANGELOG.md +++ b/packages/connectors/connector-sendgrid-email/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-sendgrid-email +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-sendgrid-email/package.json b/packages/connectors/connector-sendgrid-email/package.json index 2f7650c7078..650f4b88050 100644 --- a/packages/connectors/connector-sendgrid-email/package.json +++ b/packages/connectors/connector-sendgrid-email/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-sendgrid-email", - "version": "1.1.0", + "version": "1.1.1", "description": "SendGrid Email Service connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-sendgrid-email/src/index.test.ts b/packages/connectors/connector-sendgrid-email/src/index.test.ts index 9f518865ce4..1062b215985 100644 --- a/packages/connectors/connector-sendgrid-email/src/index.test.ts +++ b/packages/connectors/connector-sendgrid-email/src/index.test.ts @@ -1,9 +1,7 @@ import createConnector from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('SendGrid connector', () => { it('init without throwing errors', async () => { diff --git a/packages/connectors/connector-smsaero/CHANGELOG.md b/packages/connectors/connector-smsaero/CHANGELOG.md index 651102d641a..72813750cc4 100644 --- a/packages/connectors/connector-smsaero/CHANGELOG.md +++ b/packages/connectors/connector-smsaero/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-smsaero +## 1.2.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-smsaero/package.json b/packages/connectors/connector-smsaero/package.json index 2465fdefe94..da34f59e820 100644 --- a/packages/connectors/connector-smsaero/package.json +++ b/packages/connectors/connector-smsaero/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-smsaero", - "version": "1.2.0", + "version": "1.2.1", "description": "SMSAero connector implementation.", "author": "Danil Tankov ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-smsaero/src/index.test.ts b/packages/connectors/connector-smsaero/src/index.test.ts index 253b7ada79e..dd1e579457d 100644 --- a/packages/connectors/connector-smsaero/src/index.test.ts +++ b/packages/connectors/connector-smsaero/src/index.test.ts @@ -1,9 +1,7 @@ import createConnector from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('SMSAero SMS connector', () => { it('init without throwing errors', async () => { diff --git a/packages/connectors/connector-smtp/CHANGELOG.md b/packages/connectors/connector-smtp/CHANGELOG.md index c0d3cf643c0..2543676bcd0 100644 --- a/packages/connectors/connector-smtp/CHANGELOG.md +++ b/packages/connectors/connector-smtp/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-smtp +## 1.1.2 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.1 ### Patch Changes diff --git a/packages/connectors/connector-smtp/package.json b/packages/connectors/connector-smtp/package.json index 0cf61efa87a..7c07a29ee35 100644 --- a/packages/connectors/connector-smtp/package.json +++ b/packages/connectors/connector-smtp/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-smtp", - "version": "1.1.1", + "version": "1.1.2", "description": "SMTP connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0", + "@logto/connector-kit": "workspace:^3.0.0", "nodemailer": "^6.9.9" }, "devDependencies": { @@ -28,9 +28,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": { diff --git a/packages/connectors/connector-smtp/src/index.test.ts b/packages/connectors/connector-smtp/src/index.test.ts index ad442604557..a28137dd32d 100644 --- a/packages/connectors/connector-smtp/src/index.test.ts +++ b/packages/connectors/connector-smtp/src/index.test.ts @@ -16,18 +16,16 @@ import { } from './mock.js'; import { smtpConfigGuard } from './types.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConfig); - -const sendMail = jest.fn(); +const sendMail = vi.fn(); // @ts-expect-error for testing -jest.spyOn(nodemailer, 'createTransport').mockReturnValue({ sendMail } as Transporter); +vi.spyOn(nodemailer, 'createTransport').mockReturnValue({ sendMail } as Transporter); describe('SMTP connector', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('init without throwing errors', async () => { diff --git a/packages/connectors/connector-tencent-sms/CHANGELOG.md b/packages/connectors/connector-tencent-sms/CHANGELOG.md index 36b37f5da0a..9e296ec2754 100644 --- a/packages/connectors/connector-tencent-sms/CHANGELOG.md +++ b/packages/connectors/connector-tencent-sms/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-tencent-sms +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-tencent-sms/package.json b/packages/connectors/connector-tencent-sms/package.json index 9f0b2ef411f..60614fab7d5 100644 --- a/packages/connectors/connector-tencent-sms/package.json +++ b/packages/connectors/connector-tencent-sms/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-tencent-sms", - "version": "1.1.0", + "version": "1.1.1", "description": "Tencent SMS connector implementation.", "author": "StringKe", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-tencent-sms/src/index.test.ts b/packages/connectors/connector-tencent-sms/src/index.test.ts index a30b51fed2f..06e60445a37 100644 --- a/packages/connectors/connector-tencent-sms/src/index.test.ts +++ b/packages/connectors/connector-tencent-sms/src/index.test.ts @@ -2,11 +2,9 @@ import { TemplateType } from '@logto/connector-kit'; import { codeTest, mockedConnectorConfig, mockedTemplateCode, phoneTest } from './mock.js'; -const { jest } = import.meta; +const getConfig = vi.fn().mockResolvedValue(mockedConnectorConfig); -const getConfig = jest.fn().mockResolvedValue(mockedConnectorConfig); - -const sendSmsRequest = jest.fn(() => { +const sendSmsRequest = vi.fn(() => { return { body: { Response: { @@ -27,10 +25,10 @@ const sendSmsRequest = jest.fn(() => { }; }); -jest.unstable_mockModule('./http.js', () => { +vi.mock('./http.js', () => { return { sendSmsRequest, - isSmsErrorResponse: jest.fn((response) => { + isSmsErrorResponse: vi.fn((response) => { return response.Response.Error !== undefined; }), }; @@ -40,7 +38,7 @@ const { default: createConnector } = await import('./index.js'); describe('sendMessage()', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should call sendSmsRequest() and replace code in content', async () => { diff --git a/packages/connectors/connector-twilio-sms/CHANGELOG.md b/packages/connectors/connector-twilio-sms/CHANGELOG.md index 00869b4705c..bafc7348638 100644 --- a/packages/connectors/connector-twilio-sms/CHANGELOG.md +++ b/packages/connectors/connector-twilio-sms/CHANGELOG.md @@ -1,5 +1,15 @@ # @logto/connector-twilio-sms +## 1.1.1 + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-twilio-sms/package.json b/packages/connectors/connector-twilio-sms/package.json index 3fc3c5c665b..91bd74d1796 100644 --- a/packages/connectors/connector-twilio-sms/package.json +++ b/packages/connectors/connector-twilio-sms/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-twilio-sms", - "version": "1.1.0", + "version": "1.1.1", "description": "Twilio SMS connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-twilio-sms/src/index.test.ts b/packages/connectors/connector-twilio-sms/src/index.test.ts index f7f61febb5e..937403c0f1e 100644 --- a/packages/connectors/connector-twilio-sms/src/index.test.ts +++ b/packages/connectors/connector-twilio-sms/src/index.test.ts @@ -1,9 +1,7 @@ import createConnector from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('Twilio SMS connector', () => { it('init without throwing errors', async () => { diff --git a/packages/connectors/connector-wechat-native/CHANGELOG.md b/packages/connectors/connector-wechat-native/CHANGELOG.md index 41b0a5a7481..a8b63c8afc6 100644 --- a/packages/connectors/connector-wechat-native/CHANGELOG.md +++ b/packages/connectors/connector-wechat-native/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-wechat-native +## 1.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.1.0 ### Minor Changes diff --git a/packages/connectors/connector-wechat-native/package.json b/packages/connectors/connector-wechat-native/package.json index 6a9e9986efc..ffb01ca21db 100644 --- a/packages/connectors/connector-wechat-native/package.json +++ b/packages/connectors/connector-wechat-native/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-wechat-native", - "version": "1.1.0", + "version": "1.2.0", "description": "WeChat native connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-wechat-native/src/index.test.ts b/packages/connectors/connector-wechat-native/src/index.test.ts index 15a5f449ef5..b4bde0fb283 100644 --- a/packages/connectors/connector-wechat-native/src/index.test.ts +++ b/packages/connectors/connector-wechat-native/src/index.test.ts @@ -6,13 +6,11 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri', async () => { @@ -26,7 +24,7 @@ describe('getAuthorizationUri', () => { jti: 'dummy-jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?app_id=%3Capp-id%3E&state=dummy-state` @@ -37,7 +35,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const accessTokenEndpointUrl = new URL(accessTokenEndpoint); @@ -66,7 +64,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_029, errmsg: 'invalid code' }); - await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'invalid code') ); }); @@ -76,7 +74,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: 40_163, errmsg: 'code been used' }); - await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'code been used') ); }); @@ -86,7 +84,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: -1, errmsg: 'system error' }); - await expect(getAccessToken('wrong_code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('wrong_code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'system error', errcode: -1, @@ -132,30 +130,35 @@ describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const userInfoEndpointUrl = new URL(userInfoEndpoint); 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()); + const socialUserInfo = await connector.getUserInfo({ code: 'code' }, vi.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, }); }); it('throws General error if code not provided in input', async () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); @@ -170,7 +173,7 @@ describe('getUserInfo', () => { errmsg: 'missing openid', }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'missing openid', errcode: 41_009, @@ -184,7 +187,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_001, errmsg: 'invalid credential' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'invalid credential') ); }); @@ -192,7 +195,7 @@ describe('getUserInfo', () => { it('throws unrecognized error', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow(); }); it('throws Error if request failed and errcode is not 40001', async () => { @@ -201,7 +204,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_003, errmsg: 'invalid openid' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'invalid openid', errcode: 40_003, @@ -212,7 +215,7 @@ describe('getUserInfo', () => { it('throws SocialAccessTokenInvalid error if response code is 401', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) ); }); diff --git a/packages/connectors/connector-wechat-native/src/index.ts b/packages/connectors/connector-wechat-native/src/index.ts index fc976cc0931..e2fe4f9ec26 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/CHANGELOG.md b/packages/connectors/connector-wechat-web/CHANGELOG.md index 136c82791e3..c30b52bafe5 100644 --- a/packages/connectors/connector-wechat-web/CHANGELOG.md +++ b/packages/connectors/connector-wechat-web/CHANGELOG.md @@ -1,5 +1,19 @@ # @logto/connector-wechat-web +## 1.3.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 + ## 1.2.0 ### Minor Changes diff --git a/packages/connectors/connector-wechat-web/package.json b/packages/connectors/connector-wechat-web/package.json index e05743e9121..d14d5c9b49c 100644 --- a/packages/connectors/connector-wechat-web/package.json +++ b/packages/connectors/connector-wechat-web/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-wechat-web", - "version": "1.2.0", + "version": "1.3.0", "description": "Wechat Web connector implementation.", "author": "Silverhand Inc. ", "dependencies": { - "@logto/connector-kit": "workspace:^2.1.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-wechat-web/src/index.test.ts b/packages/connectors/connector-wechat-web/src/index.test.ts index 6d03e72b000..3906fed7e31 100644 --- a/packages/connectors/connector-wechat-web/src/index.test.ts +++ b/packages/connectors/connector-wechat-web/src/index.test.ts @@ -6,13 +6,11 @@ import { accessTokenEndpoint, authorizationEndpoint, userInfoEndpoint } from './ import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { @@ -26,7 +24,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); expect(authorizationUri).toEqual( `${authorizationEndpoint}?appid=%3Capp-id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fcallback&response_type=code&scope=snsapi_login&state=some_state` @@ -37,7 +35,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const accessTokenEndpointUrl = new URL(accessTokenEndpoint); @@ -66,7 +64,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_029, errmsg: 'invalid code' }); - await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'invalid code') ); }); @@ -76,7 +74,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: 40_163, errmsg: 'code been used' }); - await expect(getAccessToken('code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'code been used') ); }); @@ -86,7 +84,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: -1, errmsg: 'system error' }); - await expect(getAccessToken('wrong_code', mockedConfig)).rejects.toMatchError( + await expect(getAccessToken('wrong_code', mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'system error', errcode: -1, @@ -123,35 +121,40 @@ describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const userInfoEndpointUrl = new URL(userInfoEndpoint); 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() + vi.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, }); }); it('throws General error if code not provided in input', async () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); @@ -166,7 +169,7 @@ describe('getUserInfo', () => { errmsg: 'missing openid', }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'missing openid', errcode: 41_009, @@ -180,7 +183,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_001, errmsg: 'invalid credential' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'invalid credential') ); }); @@ -188,7 +191,7 @@ describe('getUserInfo', () => { it('throws unrecognized error', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow(); }); it('throws Error if request failed and errcode is not 40001', async () => { @@ -197,7 +200,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_003, errmsg: 'invalid openid' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'invalid openid', errcode: 40_003, @@ -208,7 +211,7 @@ describe('getUserInfo', () => { it('throws SocialAccessTokenInvalid error if response code is 401', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) ); }); diff --git a/packages/connectors/connector-wechat-web/src/index.ts b/packages/connectors/connector-wechat-web/src/index.ts index 7e1d96ff472..49541457264 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/CHANGELOG.md b/packages/connectors/connector-wecom/CHANGELOG.md new file mode 100644 index 00000000000..b96f0187cdd --- /dev/null +++ b/packages/connectors/connector-wecom/CHANGELOG.md @@ -0,0 +1,15 @@ +# @logto/connector-wecom + +## 0.2.0 + +### Minor Changes + +- 57d97a4df: return and store social connector raw data + +### Patch Changes + +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [57d97a4df] +- Updated dependencies [2c10c2423] + - @logto/connector-kit@3.0.0 diff --git a/packages/connectors/connector-wecom/package.json b/packages/connectors/connector-wecom/package.json index 622de5d67a5..40ca4af76aa 100644 --- a/packages/connectors/connector-wecom/package.json +++ b/packages/connectors/connector-wecom/package.json @@ -1,10 +1,10 @@ { "name": "@logto/connector-wecom", - "version": "0.1.0", + "version": "0.2.0", "description": "Wecom connector implementation.", "author": "Dove fork from Wechat Web connector", "dependencies": { - "@logto/connector-kit": "workspace:^2.0.0" + "@logto/connector-kit": "workspace:^3.0.0" }, "main": "./lib/index.js", "module": "./lib/index.js", @@ -24,9 +24,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": { diff --git a/packages/connectors/connector-wecom/src/index.test.ts b/packages/connectors/connector-wecom/src/index.test.ts index 474b1949df3..7f4d3a1a226 100644 --- a/packages/connectors/connector-wecom/src/index.test.ts +++ b/packages/connectors/connector-wecom/src/index.test.ts @@ -11,13 +11,11 @@ import { import createConnector, { getAccessToken } from './index.js'; import { mockedConfig } from './mock.js'; -const { jest } = import.meta; - -const getConfig = jest.fn().mockResolvedValue(mockedConfig); +const getConfig = vi.fn().mockResolvedValue(mockedConfig); describe('getAuthorizationUri', () => { afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should get a valid uri by redirectUri and state', async () => { @@ -31,7 +29,7 @@ describe('getAuthorizationUri', () => { jti: 'some_jti', headers: {}, }, - jest.fn() + vi.fn() ); const userAgent = 'some_UA'; const isWecom = userAgent.toLowerCase().includes('wxwork'); @@ -48,7 +46,7 @@ describe('getAuthorizationUri', () => { describe('getAccessToken', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const accessTokenEndpointUrl = new URL(accessTokenEndpoint); @@ -74,7 +72,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(parameters) .reply(200, { errcode: 40_029, errmsg: 'invalid code' }); - await expect(getAccessToken(mockedConfig)).rejects.toMatchError( + await expect(getAccessToken(mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'invalid code') ); }); @@ -84,7 +82,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: 40_163, errmsg: 'code been used' }); - await expect(getAccessToken(mockedConfig)).rejects.toMatchError( + await expect(getAccessToken(mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'code been used') ); }); @@ -94,7 +92,7 @@ describe('getAccessToken', () => { .get(accessTokenEndpointUrl.pathname) .query(true) .reply(200, { errcode: -1, errmsg: 'system error' }); - await expect(getAccessToken(mockedConfig)).rejects.toMatchError( + await expect(getAccessToken(mockedConfig)).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'system error', errcode: -1, @@ -128,33 +126,39 @@ describe('getUserInfo', () => { afterEach(() => { nock.cleanAll(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); const userInfoEndpointUrl = new URL(userInfoEndpoint); 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( { code: 'code', }, - jest.fn() + vi.fn() ); expect(socialUserInfo).toMatchObject({ id: 'wecom_id', avatar: '', name: 'wecom_id', + rawData: jsonResponse, }); }); it('throws General error if code not provided in input', async () => { const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({}, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({}, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, '{}') ); }); @@ -169,7 +173,7 @@ describe('getUserInfo', () => { errmsg: 'missing openid', }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'missing openid', errcode: 41_009, @@ -183,7 +187,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_001, errmsg: 'invalid credential' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, 'invalid credential') ); }); @@ -191,7 +195,7 @@ describe('getUserInfo', () => { it('throws unrecognized error', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(500); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toThrow(); + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toThrow(); }); it('throws Error if request failed and errcode is not 40001', async () => { @@ -200,7 +204,7 @@ describe('getUserInfo', () => { .query(parameters) .reply(200, { errcode: 40_003, errmsg: 'invalid openid' }); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.General, { errorDescription: 'invalid openid', errcode: 40_003, @@ -211,7 +215,7 @@ describe('getUserInfo', () => { it('throws SocialAccessTokenInvalid error if response code is 401', async () => { nock(userInfoEndpointUrl.origin).get(userInfoEndpointUrl.pathname).query(parameters).reply(401); const connector = await createConnector({ getConfig }); - await expect(connector.getUserInfo({ code: 'code' }, jest.fn())).rejects.toMatchError( + await expect(connector.getUserInfo({ code: 'code' }, vi.fn())).rejects.toStrictEqual( new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid) ); }); diff --git a/packages/connectors/connector-wecom/src/index.ts b/packages/connectors/connector-wecom/src/index.ts index 1f8ee1114c8..266f2be8453 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 (openid) { - return { id: openid, avatar: '', name: openid }; + if (!id) { + throw new Error('Both userid and openid are undefined or null.'); } - 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/connectors/templates/package.json b/packages/connectors/templates/package.json index a74f8dcdd08..d750610fe60 100644 --- a/packages/connectors/templates/package.json +++ b/packages/connectors/templates/package.json @@ -17,38 +17,35 @@ "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" }, "dependencies": { "@silverhand/essentials": "^2.9.0", "got": "^14.0.0", - "snakecase-keys": "^6.0.0", + "snakecase-keys": "^7.0.0", "zod": "^3.22.4" }, "devDependencies": { - "@jest/types": "^29.5.0", "@rollup/plugin-commonjs": "^25.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-typescript": "^11.0.0", "@silverhand/eslint-config": "5.0.0", "@silverhand/ts-config": "5.0.0", - "@types/jest": "^29.4.0", "@types/node": "^20.9.5", "@types/supertest": "^6.0.0", + "@vitest/coverage-v8": "^1.4.0", "eslint": "^8.44.0", - "jest": "^29.7.0", - "jest-matcher-specific-error": "^1.0.0", "lint-staged": "^15.0.0", "nock": "^13.2.2", "prettier": "^3.0.0", "rollup": "^4.0.0", - "rollup-plugin-summary": "^2.0.0", + "rollup-plugin-output-size": "^1.3.0", "supertest": "^6.2.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^1.4.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/connectors/templates/preset/jest.config.js b/packages/connectors/templates/preset/jest.config.js deleted file mode 100644 index 6ca9a171124..00000000000 --- a/packages/connectors/templates/preset/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - setupFilesAfterEnv: ['jest-matcher-specific-error'], - roots: ['lib'], -}; - -export default config; diff --git a/packages/connectors/templates/preset/rollup.config.js b/packages/connectors/templates/preset/rollup.config.js index 29a81ed5d06..df98e6a1c1b 100644 --- a/packages/connectors/templates/preset/rollup.config.js +++ b/packages/connectors/templates/preset/rollup.config.js @@ -2,7 +2,7 @@ import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; -import { summary } from 'rollup-plugin-summary'; +import outputSize from 'rollup-plugin-output-size'; /** * @type {import('rollup').RollupOptions} @@ -14,10 +14,10 @@ const configs = [ external: ['zod', 'got', '@logto/connector-kit'], plugins: [ typescript({ tsconfig: 'tsconfig.build.json' }), - nodeResolve({ exportConditions: ['node'] }), + nodeResolve({ exportConditions: ['node'], preferBuiltins: true }), commonjs(), json(), - summary(), + outputSize(), ], }, ]; diff --git a/packages/connectors/templates/preset/tsconfig.json b/packages/connectors/templates/preset/tsconfig.json index 40492907f59..4fa2dd684aa 100644 --- a/packages/connectors/templates/preset/tsconfig.json +++ b/packages/connectors/templates/preset/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base", "compilerOptions": { - "types": ["node", "jest", "jest-matcher-specific-error"] + "types": ["node", "vitest/globals"] }, "include": ["src", "types"] } diff --git a/packages/connectors/templates/preset/types/import-meta.d.ts b/packages/connectors/templates/preset/types/import-meta.d.ts deleted file mode 100644 index e016debb586..00000000000 --- a/packages/connectors/templates/preset/types/import-meta.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface ImportMeta { - jest: typeof jest & { - // Almost same as `jest.mock()`, but factory is required - unstable_mockModule: ( - moduleName: string, - factory: () => T, - options?: jest.MockOptions - ) => typeof jest; - }; -} diff --git a/packages/connectors/templates/preset/vitest.config.ts b/packages/connectors/templates/preset/vitest.config.ts new file mode 100644 index 00000000000..32ae898f000 --- /dev/null +++ b/packages/connectors/templates/preset/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + }, +}) diff --git a/packages/connectors/templates/sync-preset.js b/packages/connectors/templates/sync-preset.js index 1c4294d198f..65225ca181d 100644 --- a/packages/connectors/templates/sync-preset.js +++ b/packages/connectors/templates/sync-preset.js @@ -15,6 +15,11 @@ const templateJson = Object.fromEntries( ); const templateKeys = Object.keys(templateJson); +/** + * An object that contains exceptions for scripts that are allowed to be different from the template. + */ +const scriptExceptions = {}; + const sync = async () => { const packagesDirectory = './'; const packages = await fs.readdir(packagesDirectory); @@ -42,9 +47,23 @@ const sync = async () => { ); } + const scriptOverrides = scriptExceptions[packageName] + ? Object.fromEntries( + scriptExceptions[packageName].map((key) => [key, current.scripts[key]]) + ) + : {}; + await fs.writeFile( packageJsonPath, - JSON.stringify({ ...current, ...templateJson }, undefined, 2) + '\n' + JSON.stringify( + { + ...current, + ...templateJson, + scripts: { ...templateJson.scripts, ...scriptOverrides }, + }, + undefined, + 2 + ) + '\n' ); // Copy preset diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 5cf1630705e..1d632565e22 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## 1.13.0 + +### Minor Changes + +- 5758f84f5: feat(console): support signing-key rotation + +### Patch Changes + +- 746483c49: api resource indicator must be a valid absolute uri + + An invalid indicator will make Console crash without this check. + + Note: We don't mark it as a breaking change as the api behavior has not changed, only adding the check on Console. + ## 1.12.1 ### Patch Changes diff --git a/packages/console/generate.sh b/packages/console/generate.sh new file mode 100755 index 00000000000..d54dcf99a7d --- /dev/null +++ b/packages/console/generate.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Clean up +rm -rf scripts-js/ +# build the jwt-customizer-type-definition generate script +pnpm exec tsc -p tsconfig.scripts.gen.json +# clean up the existing generated jwt-customizer-type-definition file +rm -f src/consts/jwt-customizer-type-definition.ts +# run script +node scripts-js/generate-jwt-customizer-type-definition.js +# Clean up +rm -rf scripts-js/ diff --git a/packages/console/package.json b/packages/console/package.json index 64626cda817..d614198b90c 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@logto/console", - "version": "1.12.1", + "version": "1.13.0", "description": "> TODO: description", "author": "Silverhand Inc. ", "homepage": "https://github.com/logto-io/logto#readme", @@ -11,11 +11,13 @@ "dist" ], "scripts": { + "prepack": "pnpm generate", + "generate": "./generate.sh", "precommit": "lint-staged", "start": "parcel src/index.html", "dev": "cross-env PORT=5002 parcel src/index.html --public-url ${CONSOLE_PUBLIC_URL:-/console} --no-cache --hmr-port 6002", "check": "tsc --noEmit", - "build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url ${CONSOLE_PUBLIC_URL:-/console}", + "build": "pnpm generate && pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url ${CONSOLE_PUBLIC_URL:-/console}", "lint": "eslint --ext .ts --ext .tsx src", "lint:report": "pnpm lint --format json --output-file report.json", "stylelint": "stylelint \"src/**/*.scss\"", @@ -26,16 +28,17 @@ "@fontsource/roboto-mono": "^5.0.0", "@jest/types": "^29.5.0", "@logto/app-insights": "workspace:^1.4.0", - "@logto/cloud": "0.2.5-faca9a9", - "@logto/connector-kit": "workspace:^2.1.0", - "@logto/core-kit": "workspace:^2.3.0", + "@logto/cloud": "0.2.5-ab8a489", + "@logto/connector-kit": "workspace:^3.0.0", + "@logto/core-kit": "workspace:^2.4.0", "@logto/language-kit": "workspace:^1.1.0", - "@logto/phrases": "workspace:^1.9.0", - "@logto/phrases-experience": "workspace:^1.6.0", - "@logto/react": "^3.0.0", - "@logto/schemas": "workspace:^1.13.1", + "@logto/phrases": "workspace:^1.10.0", + "@logto/phrases-experience": "workspace:^1.6.1", + "@logto/react": "^3.0.5", + "@logto/schemas": "workspace:^1.15.0", "@logto/shared": "workspace:^3.1.0", "@mdx-js/react": "^1.6.22", + "@monaco-editor/react": "^4.6.0", "@parcel/compressor-brotli": "2.9.3", "@parcel/compressor-gzip": "2.9.3", "@parcel/core": "2.9.3", @@ -60,7 +63,7 @@ "@types/react-helmet": "^6.1.6", "@types/react-modal": "^3.13.1", "@types/react-syntax-highlighter": "^15.5.1", - "@withtyped/client": "^0.7.22", + "@withtyped/client": "^0.8.4", "buffer": "^5.7.1", "classnames": "^2.3.1", "clean-deep": "^3.4.0", @@ -81,7 +84,7 @@ "jest-transform-stub": "^2.0.0", "jest-transformer-svg": "^2.0.0", "just-kebab-case": "^4.2.0", - "ky": "^1.0.0", + "ky": "^1.2.3", "libphonenumber-js": "^1.10.51", "lint-staged": "^15.0.0", "nanoid": "^5.0.1", @@ -120,7 +123,8 @@ "ts-node": "^10.9.2", "tslib": "^2.4.1", "typescript": "^5.3.3", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-to-ts": "^1.2.0" }, "engines": { "node": "^20.9.0" @@ -140,6 +144,12 @@ }, "eslintConfig": { "extends": "@silverhand/react", + "parserOptions": { + "project": [ + "./tsconfig.json", + "./tsconfig.scripts.gen.json" + ] + }, "rules": { "react/function-component-definition": [ "error", diff --git a/packages/console/scripts/generate-jwt-customizer-type-definition.ts b/packages/console/scripts/generate-jwt-customizer-type-definition.ts new file mode 100644 index 00000000000..9b25077a20b --- /dev/null +++ b/packages/console/scripts/generate-jwt-customizer-type-definition.ts @@ -0,0 +1,75 @@ +import fs from 'node:fs'; + +import { + accessTokenPayloadGuard, + clientCredentialsPayloadGuard, + jwtCustomizerUserContextGuard, +} from '@logto/schemas'; +import prettier from 'prettier'; +import { type ZodTypeAny } from 'zod'; +import { printNode, zodToTs } from 'zod-to-ts'; + +const filePath = 'src/consts/jwt-customizer-type-definition.ts'; + +const typeIdentifiers = `export enum JwtCustomizerTypeDefinitionKey { + JwtCustomizerUserContext = 'JwtCustomizerUserContext', + AccessTokenPayload = 'AccessTokenPayload', + ClientCredentialsPayload = 'ClientCredentialsPayload', + EnvironmentVariables = 'EnvironmentVariables', +};`; + +const inferTsDefinitionFromZod = (zodSchema: ZodTypeAny, identifier: string): string => { + /** + * We have z.lazy() used for defining Json objects in the zod schemas. + * @see https://zod.dev/?id=json-type + * zod-to-ts does not support z.lazy() yet. It will use the root type of the lazy schema. Which will be the identifier we pass to the function. + * @see https://github.com/sachinraja/zod-to-ts?tab=readme-ov-file#zlazy + * + * The second argument is the root type identifier for the schema. + * Here we use 'Record' as the root type identifier. So all the Json objects will be inferred as Record. + * This is a limitation of zod-to-ts. We can't infer the exact type of the Json objects. + * This solution is hacky but it works for now. The impact is it will always define the type identifer as Record. + */ + const { node } = zodToTs(zodSchema, 'Record', { nativeEnums: 'union' }); + const typeDefinition = printNode(node); + + return `type ${identifier} = ${typeDefinition};`; +}; + +// Create the jwt-customizer-type-definition.ts file +const createJwtCustomizerTypeDefinitions = async () => { + const jwtCustomizerUserContextTypeDefinition = inferTsDefinitionFromZod( + jwtCustomizerUserContextGuard, + 'JwtCustomizerUserContext' + ); + + const accessTokenPayloadTypeDefinition = inferTsDefinitionFromZod( + accessTokenPayloadGuard, + 'AccessTokenPayload' + ); + + const clientCredentialsPayloadTypeDefinition = inferTsDefinitionFromZod( + clientCredentialsPayloadGuard, + 'ClientCredentialsPayload' + ); + + const fileContent = `/* This file is auto-generated. Do not modify it manually. */ +${typeIdentifiers} + +export const jwtCustomizerUserContextTypeDefinition = \`${jwtCustomizerUserContextTypeDefinition}\`; + +export const accessTokenPayloadTypeDefinition = \`${accessTokenPayloadTypeDefinition}\`; + +export const clientCredentialsPayloadTypeDefinition = \`${clientCredentialsPayloadTypeDefinition}\`; +`; + + const formattedFileContent = await prettier.format(fileContent, { + parser: 'typescript', + tabWidth: 2, + singleQuote: true, + }); + + fs.writeFileSync(filePath, formattedFileContent); +}; + +void createJwtCustomizerTypeDefinitions(); diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 007f035a99b..451560e44fb 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -1,6 +1,6 @@ import { AppInsightsBoundary } from '@logto/app-insights/react'; import { UserScope } from '@logto/core-kit'; -import { LogtoProvider, useLogto } from '@logto/react'; +import { LogtoProvider, Prompt, useLogto } from '@logto/react'; import { adminConsoleApplicationId, defaultTenantId, @@ -22,11 +22,12 @@ import CloudAppRoutes from '@/cloud/AppRoutes'; import AppLoading from '@/components/AppLoading'; import { isCloud } from '@/consts/env'; import { cloudApi, getManagementApi, meApi } from '@/consts/resources'; +import { ConsoleRoutes } from '@/containers/ConsoleRoutes'; import useTrackUserId from '@/hooks/use-track-user-id'; import { OnboardingRoutes } from '@/onboarding'; import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; -import { ConsoleRoutes } from '@/pages/ConsoleRoutes'; +import { GlobalScripts } from './components/Conversion'; import { adminTenantEndpoint, mainTitle } from './consts'; import ErrorBoundary from './containers/ErrorBoundary'; import LogtoErrorBoundary from './containers/LogtoErrorBoundary'; @@ -109,6 +110,7 @@ function Providers() { appId: adminConsoleApplicationId, resources, scopes, + prompt: [Prompt.Login, Prompt.Consent], }} > @@ -153,5 +155,10 @@ function AppRoutes() { return ; } - return isAuthenticated && isOnboarding ? : ; + return ( + <> + + {isAuthenticated && isOnboarding ? : } + + ); } diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/README.mdx b/packages/console/src/assets/docs/single-sign-on/azure-oidc/README.mdx new file mode 100644 index 00000000000..0245fe9393d --- /dev/null +++ b/packages/console/src/assets/docs/single-sign-on/azure-oidc/README.mdx @@ -0,0 +1,78 @@ +import OidcCallbackUri from '@/mdx-components/OidcCallbackUri'; +import Step from '@/mdx-components/Step'; + +import createApplication from './assets/create_application.webp'; +import configApplication from './assets/config_application.webp'; +import applicationDetails from './assets/application_details.webp'; +import createSecret from './assets/create_secret.webp'; +import endpoints from './assets/endpoints.webp'; + + + +1. Go to the [Microsoft Entra admin center](https://entra.microsoft.com/) and sign in as an administrator. + +2. Browse to Identity > Applications > App registrations. + +
+ Create Application +
+ +3. Select `New registration`. + +4. Enter the application name and select the appropriate account type for your application. + +5. Select `Web` as the application platform. Enter the redirect URI for the application. The redirect URI is the URL where the user is redirected after they have authenticated with Microsoft Entra ID. + + + +
+ Configure Application +
+ +6. Click `Register` to create the application. + +
+ + + +After successfully creating an Microsoft Entra OIDC application, you will need to provide the IdP configurations back to Logto. Navigate to the `Connection` tab at Logto console, and fill in the following configurations: + +1. **Client ID**: A unique identifier assigned to your OIDC application by the Microsoft Entra. This identifier is used by Logto to identify and authenticate the application during the OIDC flow. You can find it in the application overview page as `Application (client) ID`. + +
+ Application Details +
+ +2. **Client Secret**: Create a new client secret and copy the value to Logto. This secret is used to authenticate the OIDC application and secure the communication between Logto and the IdP. + +
+ Create Secret +
+ +3. **Issuer**: The issuer URL, a unique identifier for the IdP, specifying the location where the OIDC identity provider can be found. It is a crucial part of the OIDC configuration as it helps Logto discover the necessary endpoints. + + Instead of manually provide all these OIDC endpoints, Logto fetch all the required configurations and IdP endpoints automatically. This is done by utilizing the issuer url you provided and making a call to the IdP's discover endpoint. + + To get the issuer URL, you can find it in the `Endpoints` section of the application overview page. + + Locate the `OpenID Connect metadata document` endpoint and copy the URL **WITHOUT** the trailing path `.well-known/openid-configuration`. This is because Logto will automatically append the `.well-known/openid-configuration` to the issuer URL when fetching the OIDC configurations. + +
+ Endpoints +
+ +4. **Scope**: A space-separated list of strings defining the desired permissions or access levels requested by Logto during the OIDC authentication process. The scope parameter allows you to specify what information and access Logto is requesting from the IdP. + +The scope parameter is optional. Regardless of the custom scope settings, Logto will always send the `openid`, `profile` and `email` scopes to the IdP. + +Click `Save` to finish the configuration process + +
+ + + +Provide the email `domains` of your organization on the connector `experience` tab. This will enabled the SSO connector as an authentication method for those users. + +Users with email addresses in the specified domains will be exclusively limited to use your SSO connector as their only authentication method. + + diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/application_details.webp b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/application_details.webp new file mode 100644 index 00000000000..909c0626c8d Binary files /dev/null and b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/application_details.webp differ diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/config_application.webp b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/config_application.webp new file mode 100644 index 00000000000..9bd2bd5c9c9 Binary files /dev/null and b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/config_application.webp differ diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_application.webp b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_application.webp new file mode 100644 index 00000000000..af39e2607f2 Binary files /dev/null and b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_application.webp differ diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_secret.webp b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_secret.webp new file mode 100644 index 00000000000..1151663fbec Binary files /dev/null and b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/create_secret.webp differ diff --git a/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/endpoints.webp b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/endpoints.webp new file mode 100644 index 00000000000..895866cb3a3 Binary files /dev/null and b/packages/console/src/assets/docs/single-sign-on/azure-oidc/assets/endpoints.webp differ diff --git a/packages/console/src/assets/docs/single-sign-on/index.ts b/packages/console/src/assets/docs/single-sign-on/index.ts index 130fc3da02b..775e05e21f0 100644 --- a/packages/console/src/assets/docs/single-sign-on/index.ts +++ b/packages/console/src/assets/docs/single-sign-on/index.ts @@ -1,6 +1,6 @@ import { SsoProviderName } from '@logto/schemas'; import { type MDXProps } from 'mdx/types'; -import { lazy, type LazyExoticComponent, type FunctionComponent } from 'react'; +import { lazy, type FunctionComponent, type LazyExoticComponent } from 'react'; type GuideComponentType = LazyExoticComponent>; @@ -10,6 +10,7 @@ const ssoConnectorGuides: Readonly<{ [key in SsoProviderName]?: GuideComponentTy [SsoProviderName.AZURE_AD]: lazy(async () => import('./azure-ad/README.mdx')), [SsoProviderName.GOOGLE_WORKSPACE]: lazy(async () => import('./google-workspace/README.mdx')), [SsoProviderName.OKTA]: lazy(async () => import('./okta/README.mdx')), + [SsoProviderName.AZURE_AD_OIDC]: lazy(async () => import('./azure-oidc/README.mdx')), }; export default ssoConnectorGuides; diff --git a/packages/console/src/assets/icons/book.svg b/packages/console/src/assets/icons/book.svg new file mode 100644 index 00000000000..d91a3473790 --- /dev/null +++ b/packages/console/src/assets/icons/book.svg @@ -0,0 +1,4 @@ + + + diff --git a/packages/console/src/assets/icons/conical-flask.svg b/packages/console/src/assets/icons/conical-flask.svg new file mode 100644 index 00000000000..2059208bd87 --- /dev/null +++ b/packages/console/src/assets/icons/conical-flask.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/console/src/assets/icons/invitation.svg b/packages/console/src/assets/icons/invitation.svg new file mode 100644 index 00000000000..2c245c59f2e --- /dev/null +++ b/packages/console/src/assets/icons/invitation.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/assets/icons/jwt-claims.svg b/packages/console/src/assets/icons/jwt-claims.svg new file mode 100644 index 00000000000..82fb6e07c37 --- /dev/null +++ b/packages/console/src/assets/icons/jwt-claims.svg @@ -0,0 +1,4 @@ + + + diff --git a/packages/console/src/assets/icons/key.svg b/packages/console/src/assets/icons/key.svg new file mode 100644 index 00000000000..821b2ce867f --- /dev/null +++ b/packages/console/src/assets/icons/key.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/assets/icons/members.svg b/packages/console/src/assets/icons/members.svg new file mode 100644 index 00000000000..be8a9de010d --- /dev/null +++ b/packages/console/src/assets/icons/members.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/assets/icons/research.svg b/packages/console/src/assets/icons/research.svg new file mode 100644 index 00000000000..89105ca6de1 --- /dev/null +++ b/packages/console/src/assets/icons/research.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/assets/icons/start.svg b/packages/console/src/assets/icons/start.svg new file mode 100644 index 00000000000..8be0c28ffdc --- /dev/null +++ b/packages/console/src/assets/icons/start.svg @@ -0,0 +1,4 @@ + + + diff --git a/packages/console/src/assets/icons/token-file-icon.svg b/packages/console/src/assets/icons/token-file-icon.svg new file mode 100644 index 00000000000..8860e4bf2c2 --- /dev/null +++ b/packages/console/src/assets/icons/token-file-icon.svg @@ -0,0 +1,5 @@ + + + + diff --git a/packages/console/src/assets/icons/user-file-icon.svg b/packages/console/src/assets/icons/user-file-icon.svg new file mode 100644 index 00000000000..2e2da53803e --- /dev/null +++ b/packages/console/src/assets/icons/user-file-icon.svg @@ -0,0 +1,4 @@ + + + diff --git a/packages/console/src/assets/icons/user.svg b/packages/console/src/assets/icons/user.svg index d86997a3186..ee2e0136f84 100644 --- a/packages/console/src/assets/icons/user.svg +++ b/packages/console/src/assets/icons/user.svg @@ -1,3 +1,4 @@ - - + + diff --git a/packages/console/src/cloud/AppRoutes.tsx b/packages/console/src/cloud/AppRoutes.tsx index e133831edac..3230e90cc91 100644 --- a/packages/console/src/cloud/AppRoutes.tsx +++ b/packages/console/src/cloud/AppRoutes.tsx @@ -1,7 +1,9 @@ import { Route, Routes } from 'react-router-dom'; +import { isCloud } from '@/consts/env'; import ProtectedRoutes from '@/containers/ProtectedRoutes'; import { GlobalAnonymousRoute, GlobalRoute } from '@/contexts/TenantsProvider'; +import AcceptInvitation from '@/pages/AcceptInvitation'; import Callback from '@/pages/Callback'; import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback'; @@ -17,6 +19,12 @@ function AppRoutes() { } /> } /> }> + {isCloud && ( + } + /> + )} } /> } /> diff --git a/packages/console/src/cloud/hooks/use-cloud-api.ts b/packages/console/src/cloud/hooks/use-cloud-api.ts index 56c9b9cf2bb..350928b7184 100644 --- a/packages/console/src/cloud/hooks/use-cloud-api.ts +++ b/packages/console/src/cloud/hooks/use-cloud-api.ts @@ -1,12 +1,15 @@ import type router from '@logto/cloud/routes'; +import { type tenantAuthRouter } from '@logto/cloud/routes'; import { useLogto } from '@logto/react'; +import { getTenantOrganizationId } from '@logto/schemas'; import { conditional, trySafe } from '@silverhand/essentials'; import Client, { ResponseError } from '@withtyped/client'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { toast } from 'react-hot-toast'; import { z } from 'zod'; import { cloudApi } from '@/consts'; +import { TenantsContext } from '@/contexts/TenantsProvider'; const responseErrorBodyGuard = z.object({ message: z.string(), @@ -57,3 +60,34 @@ export const useCloudApi = ({ hideErrorToast = false }: UseCloudApiProps = {}): return api; }; + +/** + * This hook is used to request the cloud `tenantAuthRouter` endpoints, with an organization token. + */ +export const useAuthedCloudApi = ({ hideErrorToast = false }: UseCloudApiProps = {}): Client< + typeof tenantAuthRouter +> => { + const { currentTenantId } = useContext(TenantsContext); + const { isAuthenticated, getOrganizationToken } = useLogto(); + const api = useMemo( + () => + new Client({ + baseUrl: window.location.origin, + headers: async () => { + if (isAuthenticated) { + return { + Authorization: `Bearer ${ + (await getOrganizationToken(getTenantOrganizationId(currentTenantId))) ?? '' + }`, + }; + } + }, + before: { + ...conditional(!hideErrorToast && { error: toastResponseError }), + }, + }), + [currentTenantId, getOrganizationToken, hideErrorToast, isAuthenticated] + ); + + return api; +}; diff --git a/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss b/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss new file mode 100644 index 00000000000..3f223613fc5 --- /dev/null +++ b/packages/console/src/cloud/pages/Main/InvitationList/index.module.scss @@ -0,0 +1,71 @@ +@use '@/scss/underscore' as _; + +.container { + display: flex; + flex-direction: column; + height: 100%; + min-height: 600px; + background: var(--color-base); + align-items: center; + justify-content: center; + overflow-y: auto; + + .wrapper { + display: flex; + flex-direction: column; + width: 540px; + padding: _.unit(20) _.unit(17.5); + gap: _.unit(6); + background: var(--color-layer-1); + border-radius: 16px; + box-shadow: var(--shadow-1); + white-space: pre-wrap; + + .title { + font: var(--font-headline-2); + } + + .description { + font: var(--font-body-2); + color: var(--color-text-secondary); + } + + .tenant { + display: flex; + align-items: center; + padding: _.unit(3) _.unit(4); + gap: _.unit(3); + border-radius: 12px; + border: 1px solid var(--color-divider); + + .name { + @include _.multi-line-ellipsis(2); + } + + .tag { + margin-left: _.unit(-2); + } + } + + .separator { + display: flex; + align-items: center; + gap: _.unit(4); + + span { + font: var(--font-body-2); + color: var(--color-text-secondary); + } + + hr { + flex: 1; + border: none; + border-top: 1px solid var(--color-divider); + } + } + + .createTenantButton { + width: 100%; + } + } +} diff --git a/packages/console/src/cloud/pages/Main/InvitationList/index.tsx b/packages/console/src/cloud/pages/Main/InvitationList/index.tsx new file mode 100644 index 00000000000..db7bd5317dc --- /dev/null +++ b/packages/console/src/cloud/pages/Main/InvitationList/index.tsx @@ -0,0 +1,90 @@ +import { OrganizationInvitationStatus, getTenantIdFromOrganizationId } from '@logto/schemas'; +import { useContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import OrganizationIcon from '@/assets/icons/organization-preview.svg'; +import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; +import { type InvitationListResponse } from '@/cloud/types/router'; +import TenantEnvTag from '@/components/TenantEnvTag'; +import ThemedIcon from '@/components/ThemedIcon'; +import { TenantsContext } from '@/contexts/TenantsProvider'; +import Button from '@/ds-components/Button'; +import Spacer from '@/ds-components/Spacer'; +import useTenantPathname from '@/hooks/use-tenant-pathname'; +import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; + +import * as styles from './index.module.scss'; + +type Props = { + invitations: InvitationListResponse; +}; + +function InvitationList({ invitations }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const cloudApi = useCloudApi(); + const { navigateTenant, resetTenants } = useContext(TenantsContext); + const { navigate } = useTenantPathname(); + const [isJoining, setIsJoining] = useState(false); + const [isUpdatingOnboardingStatus, setIsUpdatingOnboardingStatus] = useState(false); + const { update } = useUserOnboardingData(); + + return ( +
+
+
{t('invitation.find_your_tenants')}
+
{t('invitation.find_tenants_description')}
+ {invitations.map(({ id, organizationId, tenantName, tenantTag }) => ( +
+ + {tenantName} + + +
+ ))} +
+
+ {t('general.or')} +
+
+
+
+ ); +} + +export default InvitationList; diff --git a/packages/console/src/cloud/pages/Main/index.tsx b/packages/console/src/cloud/pages/Main/index.tsx index 51cc2de313f..42947a5562d 100644 --- a/packages/console/src/cloud/pages/Main/index.tsx +++ b/packages/console/src/cloud/pages/Main/index.tsx @@ -1,9 +1,14 @@ +import { OrganizationInvitationStatus } from '@logto/schemas'; + import AppLoading from '@/components/AppLoading'; +import { isCloud } from '@/consts/env'; import useCurrentUser from '@/hooks/use-current-user'; import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id'; +import useUserInvitations from '@/hooks/use-user-invitations'; import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; import AutoCreateTenant from './AutoCreateTenant'; +import InvitationList from './InvitationList'; import Redirect from './Redirect'; import TenantLandingPage from './TenantLandingPage'; @@ -11,6 +16,7 @@ export default function Main() { const { isLoaded } = useCurrentUser(); const { isOnboarding } = useUserOnboardingData(); const { defaultTenantId } = useUserDefaultTenantId(); + const { data } = useUserInvitations(OrganizationInvitationStatus.Pending); if (!isLoaded) { return ; @@ -26,6 +32,11 @@ export default function Main() { return ; } + // If user has pending invitations (onboarding will be skipped), show the invitation list and allow them to quick join. + if (isCloud && data?.length) { + return ; + } + // If user has completed onboarding and still has no tenant, redirect to a special landing page. return ; } diff --git a/packages/console/src/cloud/types/router.ts b/packages/console/src/cloud/types/router.ts index f741b0abd93..ed175b7afda 100644 --- a/packages/console/src/cloud/types/router.ts +++ b/packages/console/src/cloud/types/router.ts @@ -1,7 +1,9 @@ import type router from '@logto/cloud/routes'; +import { type tenantAuthRouter } from '@logto/cloud/routes'; import { type GuardedResponse, type RouterRoutes } from '@withtyped/client'; type GetRoutes = RouterRoutes['get']; +type GetTenantAuthRoutes = RouterRoutes['get']; export type GetArrayElementType = T extends Array ? U : never; @@ -15,5 +17,19 @@ export type SubscriptionUsage = GuardedResponse; +export type InvitationResponse = GuardedResponse; + +export type InvitationListResponse = GuardedResponse; + // The response of GET /api/tenants is TenantResponse[]. export type TenantResponse = GetArrayElementType>; + +// Start of the auth routes types. Accessing the auth routes requires an organization token. +export type TenantMemberResponse = GetArrayElementType< + GuardedResponse +>; + +export type TenantInvitationResponse = GetArrayElementType< + GuardedResponse +>; +// End of the auth routes types diff --git a/packages/console/src/components/ActionsButton/index.tsx b/packages/console/src/components/ActionsButton/index.tsx index 65f4c698e4c..41d6bac0e8b 100644 --- a/packages/console/src/components/ActionsButton/index.tsx +++ b/packages/console/src/components/ActionsButton/index.tsx @@ -13,8 +13,10 @@ import useActionTranslation from '@/hooks/use-action-translation'; import * as styles from './index.module.scss'; type Props = { - /** A function that will be called when the user confirms the deletion. */ - onDelete: () => void | Promise; + /** A function that will be called when the user confirms the deletion. If not provided, + * the delete button will not be displayed. + */ + onDelete?: () => void | Promise; /** * A function that will be called when the user clicks the edit button. If not provided, * the edit button will not be displayed. @@ -48,6 +50,10 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv const [isDeleting, setIsDeleting] = useState(false); const handleDelete = useCallback(async () => { + if (!onDelete) { + return; + } + setIsDeleting(true); try { await onDelete(); @@ -69,31 +75,35 @@ function ActionsButton({ onDelete, onEdit, deleteConfirmation, fieldName, textOv )} )} - } - type="danger" - onClick={() => { - setIsModalOpen(true); + {onDelete && ( + } + type="danger" + onClick={() => { + setIsModalOpen(true); + }} + > + {textOverrides?.delete ? ( + + ) : ( + tAction('delete', fieldName) + )} + + )} + + {onDelete && ( + { + setIsModalOpen(false); }} + onConfirm={handleDelete} > - {textOverrides?.delete ? ( - - ) : ( - tAction('delete', fieldName) - )} - - - { - setIsModalOpen(false); - }} - onConfirm={handleDelete} - > - - + + + )} ); } diff --git a/packages/console/src/components/Conversion/index.tsx b/packages/console/src/components/Conversion/index.tsx new file mode 100644 index 00000000000..450249c1c25 --- /dev/null +++ b/packages/console/src/components/Conversion/index.tsx @@ -0,0 +1,117 @@ +import { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; + +import useCurrentUser from '@/hooks/use-current-user'; + +import { useRetry } from './use-retry'; +import { + shouldReport, + gtagAwTrackingId, + redditPixelId, + hashEmail, + type GtagConversionId, + type RedditReportType, + reportToGoogle, + reportToReddit, +} from './utils'; + +type ScriptProps = { + userEmailHash?: string; +}; + +function GoogleScripts({ userEmailHash }: ScriptProps) { + if (!userEmailHash) { + return null; + } + + return ( + + + + ); +} + +function RedditScripts({ userEmailHash }: ScriptProps) { + if (!userEmailHash) { + return null; + } + + return ( + + + + ); +} + +/** + * Renders global scripts for conversion tracking. + */ +export function GlobalScripts() { + const { user, isLoaded } = useCurrentUser(); + const [userEmailHash, setUserEmailHash] = useState(); + + /** + * Use user email to prevent duplicate conversion, and it is hashed before sending + * to protect user privacy. + */ + useEffect(() => { + const init = async () => { + setUserEmailHash(await hashEmail(user?.primaryEmail ?? undefined)); + }; + + if (isLoaded) { + void init(); + } + }, [user, isLoaded]); + + if (!shouldReport) { + return null; + } + + return ( + <> + + + + ); +} + +type ReportConversionOptions = { + transactionId?: string; + gtagId?: GtagConversionId; + redditType?: RedditReportType; +}; + +export const useReportConversion = ({ + gtagId, + redditType, + transactionId, +}: ReportConversionOptions) => { + useRetry({ + precondition: Boolean(shouldReport && gtagId), + execute: () => (gtagId ? reportToGoogle(gtagId, { transactionId }) : false), + }); + + useRetry({ + precondition: Boolean(shouldReport && redditType), + execute: () => (redditType ? reportToReddit(redditType) : false), + }); +}; diff --git a/packages/console/src/components/Conversion/use-retry.ts b/packages/console/src/components/Conversion/use-retry.ts new file mode 100644 index 00000000000..da7c73f0d00 --- /dev/null +++ b/packages/console/src/components/Conversion/use-retry.ts @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; + +type UseRetryOptions = { + /** The precondition to check before executing the function. */ + precondition: boolean; + /** The function to execute when the precondition is not met. */ + onPreconditionFailed?: () => void; + /** The function to execute. If it returns `true`, the retry will stop. */ + execute: () => boolean; + /** + * The maximum number of retries. + * + * @default 3 + */ + maxRetry?: number; +}; + +/** + * A hook to retry a function until the condition is met. The retry interval is 1 second. + */ +export const useRetry = ({ + precondition, + onPreconditionFailed, + execute, + maxRetry = 3, +}: UseRetryOptions) => { + useEffect(() => { + if (!precondition) { + onPreconditionFailed?.(); + } + }, [onPreconditionFailed, precondition]); + + useEffect(() => { + if (!precondition) { + return; + } + + // eslint-disable-next-line @silverhand/fp/no-let + let retry = 0; + const interval = setInterval(() => { + if (execute() || retry >= maxRetry) { + clearInterval(interval); + } + // eslint-disable-next-line @silverhand/fp/no-mutation + retry += 1; + }, 1000); + + return () => { + clearInterval(interval); + }; + }, [execute, maxRetry, precondition]); +}; diff --git a/packages/console/src/components/ReportConversion/utils.test.ts b/packages/console/src/components/Conversion/utils.test.ts similarity index 100% rename from packages/console/src/components/ReportConversion/utils.test.ts rename to packages/console/src/components/Conversion/utils.test.ts diff --git a/packages/console/src/components/Conversion/utils.ts b/packages/console/src/components/Conversion/utils.ts new file mode 100644 index 00000000000..16b73f467af --- /dev/null +++ b/packages/console/src/components/Conversion/utils.ts @@ -0,0 +1,106 @@ +import { cond } from '@silverhand/essentials'; + +import { isProduction } from '@/consts/env'; + +export const gtagAwTrackingId = 'AW-11124811245'; +export enum GtagConversionId { + /** This ID indicates a user has truly signed up for Logto Cloud. */ + SignUp = 'AW-11192640559/ZuqUCLvNpasYEK_IiNkp', + /** This ID indicates a user has created a production tenant. */ + CreateProductionTenant = 'AW-11192640559/m04fCMDrxI0ZEK_IiNkp', + /** This ID indicates a user has purchased a Pro plan. */ + PurchaseProPlan = 'AW-11192640559/WjCtCKHCtpgZEK_IiNkp', +} + +export const redditPixelId = 't2_ggt11omdo'; + +const logtoProductionHostname = 'logto.io'; + +/** + * Due to the special of conversion reporting, it should be `true` only in the + * Logto Cloud production environment. + * Add the leading '.' to make it safer (ignore hostnames like "foologto.io"). + */ +export const shouldReport = window.location.hostname.endsWith('.' + logtoProductionHostname); + +const sha256 = async (message: string) => { + const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(message)); + // https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex + return [...new Uint8Array(hash)].map((value) => value.toString(16).padStart(2, '0')).join(''); +}; + +/** + * This function will do the following things: + * + * 1. Canonicalize the given email by Reddit's rule: lowercase, trim, + * remove dots, remove everything after the first '+'. + * 2. Hash the canonicalized email by SHA256. + */ +export const hashEmail = async (email?: string) => { + if (!email) { + return; + } + + const splitEmail = email.toLocaleLowerCase().trim().split('@'); + const [localPart, domain] = splitEmail; + + if (!localPart || !domain || splitEmail.length > 2) { + return; + } + + // eslint-disable-next-line unicorn/prefer-string-replace-all + const canonicalizedEmail = `${localPart.replace(/\./g, '').replace(/\+.*/, '')}@${domain}`; + return sha256(canonicalizedEmail); +}; + +/** Print debug message if not in production. */ +const debug = (...args: Parameters<(typeof console)['debug']>) => { + if (!isProduction) { + console.debug(...args); + } +}; + +/** + * Add more if needed: https://reddit.my.site.com/helpcenter/s/article/Install-the-Reddit-Pixel-on-your-website + */ +export type RedditReportType = + | 'PageVisit' + | 'ViewContent' + | 'Search' + | 'Purchase' + | 'Lead' + | 'SignUp'; + +export const reportToReddit = (redditType: RedditReportType) => { + if (!window.rdt) { + return false; + } + + debug('report:', 'redditType =', redditType); + window.rdt('track', redditType); + + return true; +}; + +export const reportToGoogle = ( + gtagId: GtagConversionId, + { transactionId }: { transactionId?: string } = {} +) => { + if (!window.gtag) { + return false; + } + + const run = async () => { + const transaction = cond(transactionId && { transaction_id: await sha256(transactionId) }); + + debug('report:', 'gtagId =', gtagId, 'transaction =', transaction); + window.gtag?.('event', 'conversion', { + send_to: gtagId, + ...transaction, + }); + }; + + void run(); + + return true; +}; diff --git a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/PlanCardItem/FeaturedPlanContent/use-featured-plan-content.ts b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/PlanCardItem/FeaturedPlanContent/use-featured-plan-content.ts index 88fbed207f8..6c4e73559b6 100644 --- a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/PlanCardItem/FeaturedPlanContent/use-featured-plan-content.ts +++ b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/PlanCardItem/FeaturedPlanContent/use-featured-plan-content.ts @@ -3,6 +3,15 @@ import { cond } from '@silverhand/essentials'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { + freePlanAuditLogsRetentionDays, + freePlanM2mLimit, + freePlanMauLimit, + freePlanPermissionsLimit, + freePlanRoleLimit, + proPlanAuditLogsRetentionDays, +} from '@/consts/subscriptions'; + type ContentData = { title: string; isAvailable: boolean; @@ -19,11 +28,11 @@ const useFeaturedPlanContent = (planId: string) => { return [ { - title: t(`mau.${planPhraseKey}`, { ...cond(isFreePlan && { count: 50_000 }) }), + title: t(`mau.${planPhraseKey}`, { ...cond(isFreePlan && { count: freePlanMauLimit }) }), isAvailable: true, }, { - title: t(`m2m.${planPhraseKey}`, { ...cond(isFreePlan && { count: 1 }) }), + title: t(`m2m.${planPhraseKey}`, { ...cond(isFreePlan && { count: freePlanM2mLimit }) }), isAvailable: true, }, { @@ -42,8 +51,8 @@ const useFeaturedPlanContent = (planId: string) => { title: t(`role_and_permissions.${planPhraseKey}`, { ...cond( isFreePlan && { - roleCount: 1, - permissionCount: 1, + roleCount: freePlanRoleLimit, + permissionCount: freePlanPermissionsLimit, } ), }), @@ -54,7 +63,9 @@ const useFeaturedPlanContent = (planId: string) => { isAvailable: !isFreePlan, }, { - title: t('audit_logs', { count: isFreePlan ? 3 : 14 }), + title: t('audit_logs', { + count: isFreePlan ? freePlanAuditLogsRetentionDays : proPlanAuditLogsRetentionDays, + }), isAvailable: true, }, ]; diff --git a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/index.tsx b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/index.tsx index 7c4fc8ccfa3..8609fede8b7 100644 --- a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/index.tsx +++ b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/index.tsx @@ -5,6 +5,7 @@ import Modal from 'react-modal'; import { useCloudApi, toastResponseError } from '@/cloud/hooks/use-cloud-api'; import { type TenantResponse } from '@/cloud/types/router'; +import { GtagConversionId, reportToGoogle } from '@/components/Conversion/utils'; import { pricingLink } from '@/consts'; import DangerousRaw from '@/ds-components/DangerousRaw'; import ModalLayout from '@/ds-components/ModalLayout'; @@ -43,6 +44,7 @@ function SelectTenantPlanModal({ tenantData, onClose }: Props) { const { name, tag } = tenantData; const newTenant = await cloudApi.post('/api/tenants', { body: { name, tag } }); + reportToGoogle(GtagConversionId.CreateProductionTenant, { transactionId: newTenant.id }); onClose(newTenant); return; } diff --git a/packages/console/src/components/ItemPreview/UserPreview.tsx b/packages/console/src/components/ItemPreview/UserPreview.tsx index 7f16a3d490e..357e74fb0ed 100644 --- a/packages/console/src/components/ItemPreview/UserPreview.tsx +++ b/packages/console/src/components/ItemPreview/UserPreview.tsx @@ -1,4 +1,4 @@ -import { type User } from '@logto/schemas'; +import { type UserInfo } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import SuspendedTag from '@/pages/Users/components/SuspendedTag'; @@ -9,17 +9,32 @@ import UserAvatar from '../UserAvatar'; import ItemPreview from '.'; type Props = { - user: User; + /** + * A subset of User schema type that is used in the preview component. + */ + user: { + id: UserInfo['id']; + avatar?: UserInfo['avatar']; + name?: UserInfo['name']; + primaryEmail?: UserInfo['primaryEmail']; + primaryPhone?: UserInfo['primaryPhone']; + username?: UserInfo['username']; + isSuspended?: UserInfo['isSuspended']; + }; + /** + * Whether to provide a link to user details page. Explicitly set to `false` to hide it. + */ + hasUserDetailsLink?: false; }; /** A component that renders a preview of a user. It's useful for displaying a user in a list. */ -function UserPreview({ user }: Props) { +function UserPreview({ user, hasUserDetailsLink }: Props) { return ( } - to={`/users/${user.id}`} + to={conditional(hasUserDetailsLink !== false && `/users/${user.id}`)} suffix={conditional(user.isSuspended && )} /> ); diff --git a/packages/console/src/components/ItemPreview/index.module.scss b/packages/console/src/components/ItemPreview/index.module.scss index 3a09bf4d61c..ee9ce986c92 100644 --- a/packages/console/src/components/ItemPreview/index.module.scss +++ b/packages/console/src/components/ItemPreview/index.module.scss @@ -31,9 +31,12 @@ .title { display: block; font: var(--font-body-2); - color: var(--color-text-link); text-decoration: none; @include _.text-ellipsis; + + &.withLink { + color: var(--color-text-link); + } } .subtitle { diff --git a/packages/console/src/components/ItemPreview/index.tsx b/packages/console/src/components/ItemPreview/index.tsx index 5e6d4544f52..ae29ac65697 100644 --- a/packages/console/src/components/ItemPreview/index.tsx +++ b/packages/console/src/components/ItemPreview/index.tsx @@ -27,7 +27,7 @@ function ItemPreview({ title, subtitle, icon, to, size = 'default', suffix, toTa
{to && ( { diff --git a/packages/console/src/components/ManageOrganizationPermissionModal/index.tsx b/packages/console/src/components/ManageOrganizationPermissionModal/index.tsx new file mode 100644 index 00000000000..d8c5ce1a8ec --- /dev/null +++ b/packages/console/src/components/ManageOrganizationPermissionModal/index.tsx @@ -0,0 +1,114 @@ +import { type OrganizationScope } from '@logto/schemas'; +import { cond, type Nullable } from '@silverhand/essentials'; +import { useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import ReactModal from 'react-modal'; + +import Button from '@/ds-components/Button'; +import FormField from '@/ds-components/FormField'; +import ModalLayout from '@/ds-components/ModalLayout'; +import TextInput from '@/ds-components/TextInput'; +import useApi from '@/hooks/use-api'; +import * as modalStyles from '@/scss/modal.module.scss'; +import { trySubmitSafe } from '@/utils/form'; + +type Props = { + /** + * The organization permission data to edit. If null, the modal will be in create mode. + */ + data: Nullable; + onClose: () => void; +}; + +type FormData = Pick; + +const organizationScopesPath = 'api/organization-scopes'; + +/** A modal that allows users to create or edit an organization permission. */ +function ManageOrganizationPermissionModal({ data, onClose }: Props) { + const isCreateMode = data === null; + + const { t } = useTranslation(undefined, { + keyPrefix: 'admin_console', + }); + + const api = useApi(); + + const { + register, + formState: { errors, isSubmitting }, + handleSubmit, + } = useForm(cond(data && { defaultValues: data })); + + const submit = handleSubmit( + trySubmitSafe(async (json) => { + await (isCreateMode + ? api.post(organizationScopesPath, { + json, + }) + : api.patch(`${organizationScopesPath}/${data.id}`, { + json, + })); + toast.success( + t(isCreateMode ? 'organization_template.permissions.created' : 'general.saved', { + name: json.name, + }) + ); + onClose(); + }) + ); + + return ( + + + {!isCreateMode && ( +