diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47e913c44..037820f5b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,5 @@ /packages/core @simeng-li @wangsijie @gao-sun /packages/console @wangsijie @charIeszhao /packages/ui @simeng-li @charIeszhao -/packages/cloud @simeng-li /packages/integration-tests @simeng-li /.changeset @gao-sun diff --git a/.github/workflows/clean-up-images.yml b/.github/workflows/clean-up-images.yml deleted file mode 100644 index 3e3bd8a5a..000000000 --- a/.github/workflows/clean-up-images.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Clean Up Images - -on: - schedule: - - cron: '0 0 * * *' # every day at midnight - -jobs: - clean-ghcr: - name: Clean up untagged images - runs-on: ubuntu-latest - steps: - - name: Delete untagged images older than three days - uses: snok/container-retention-policy@v2 - with: - image-names: logto - cut-off: Three days ago UTC - account-type: org - org-name: logto-io - keep-at-least: 1 - filter-tags: "sha-*" - token: ${{ secrets.BOT_PAT }} diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 586f05126..21c49ec4f 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -13,11 +13,6 @@ concurrency: jobs: package: - # See https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#expanding-or-adding-matrix-configurations - strategy: - matrix: - env: [oss, cloud] - runs-on: ubuntu-latest steps: @@ -27,26 +22,13 @@ jobs: uses: silverhand-io/actions-node-pnpm-run-steps@v3 - name: Build and package - if: matrix.env != 'cloud' run: | pnpm -r build ./.scripts/package.sh - - name: Build and package (Cloud) - if: matrix.env == 'cloud' - run: | - pnpm -r build - ./.scripts/package.sh - env: - IS_CLOUD: 1 - CONSOLE_PUBLIC_URL: / - ADMIN_ENDPOINT: http://localhost:3002 - # Setup ApplicationInsights for smoke testing. Note the connection string should be a dev string. - APPLICATIONINSIGHTS_CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }} - - uses: actions/upload-artifact@v3 with: - name: integration-test-${{ github.sha }}-${{ matrix.env }} + name: integration-test-${{ github.sha }} path: /tmp/logto.tar.gz retention-days: 3 @@ -54,14 +36,12 @@ jobs: strategy: fail-fast: false matrix: - target: [api, api-cloud, ui, ui-cloud] + target: [api, ui] needs: package runs-on: ubuntu-latest env: INTEGRATION_TEST: true - IS_CLOUD: ${{ contains(matrix.target, 'cloud') && '1' || '0' }} - PATH_BASED_MULTI_TENANCY: ${{ contains(matrix.target, 'cloud') && '1' || '0' }} DB_URL: postgres://postgres:postgres@localhost:5432/postgres steps: @@ -100,15 +80,14 @@ jobs: - uses: actions/download-artifact@v3 with: - name: integration-test-${{ github.sha }}-${{ contains(matrix.target, 'cloud') && 'cloud' || 'oss' }} + name: integration-test-${{ github.sha }} - name: Extract working-directory: tests run: | npm run cli init -- \ -p ../logto \ - --du ../logto.tar.gz \ - ${{ contains(matrix.target, 'cloud') && '--cloud' || '' }} + --du ../logto.tar.gz - name: Check and add mock connectors working-directory: tests @@ -128,11 +107,6 @@ jobs: env: REDIS_URL: 1 - - name: Run Logto Cloud - working-directory: logto/ - if: contains(matrix.target, 'cloud') - run: nohup npm run start:cloud > nohup-cloud.out 2> nohup-cloud.err < /dev/null & - - name: Sleep for 5 seconds run: sleep 5 @@ -151,13 +125,3 @@ jobs: - name: Show error logs working-directory: logto/ run: cat nohup.err - - - name: Show cloud logs - working-directory: logto/ - if: contains(matrix.target, 'cloud') - run: cat nohup-cloud.out - - - name: Show cloud error logs - working-directory: logto/ - if: contains(matrix.target, 'cloud') - run: cat nohup-cloud.err diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7abdf1aa2..aa13b886f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,12 +87,6 @@ jobs: build-args: | # Test cloud build additional_connector_args=--cloud - - name: Build cloud - uses: docker/build-push-action@v4 - with: - file: Dockerfile.cloud - context: . - main-alteration: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a867c57f3..b69f21af3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,16 +2,6 @@ name: Release on: workflow_dispatch: - inputs: - target: - description: 'The release target of Logto' - required: true - default: dev - type: choice - options: - - prod - - dev - push: branches: - master @@ -20,10 +10,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} jobs: - dockerize-core: - environment: ${{ startsWith(github.ref, 'refs/tags/') && 'release' || '' }} - # Use normal machine for OSS release since we'll build on Depot - runs-on: ${{ (inputs.target || 'dev') == 'dev' && 'ubuntu-latest' || 'ubuntu-latest-4-cores' }} + dockerize: + if: ${{ startsWith(github.ref, 'refs/tags/') }} + environment: 'release' + runs-on: 'ubuntu-latest' permissions: contents: read id-token: write @@ -45,7 +35,6 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=raw,value=${{ inputs.target || 'dev' }},enable=${{ !startsWith(github.ref, 'refs/tags/') }} type=edge - name: Login to DockerHub @@ -60,29 +49,11 @@ jobs: registry: ghcr.io username: silverhand-bot password: ${{ secrets.BOT_PAT }} - - - name: Setup Docker Buildx - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - uses: docker/setup-buildx-action@v2 - - - name: Build and push - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - additional_connector_args=--cloud - applicationinsights_connection_string=${{ (inputs.target || 'dev') == 'dev' && secrets.APPLICATIONINSIGHTS_CONNECTION_STRING || secrets.APPLICATIONINSIGHTS_CONNECTION_STRING_PROD }} - name: Setup Depot - if: ${{ startsWith(github.ref, 'refs/tags/') }} uses: depot/setup-action@v1 - name: Build and push - if: ${{ startsWith(github.ref, 'refs/tags/') }} uses: depot/build-push-action@v1 with: platforms: linux/amd64, linux/arm64 @@ -91,118 +62,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - - dockerize-cloud: - # Use normal machine for OSS release since we'll build on Depot - runs-on: ${{ (inputs.target || 'dev') == 'dev' && 'ubuntu-latest' || 'ubuntu-latest-4-cores' }} - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - ghcr.io/logto-io/cloud - # https://github.com/docker/metadata-action - tags: | - type=raw,value=${{ inputs.target || 'dev' }},enable=${{ !startsWith(github.ref, 'refs/tags/') }} - type=edge - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: silverhand-bot - password: ${{ secrets.BOT_PAT }} - - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and push - uses: docker/build-push-action@v4 - with: - file: Dockerfile.cloud - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - applicationinsights_connection_string=${{ (inputs.target || 'dev') == 'dev' && secrets.APPLICATIONINSIGHTS_CONNECTION_STRING || secrets.APPLICATIONINSIGHTS_CONNECTION_STRING_PROD }} - - deploy: - strategy: - fail-fast: false - matrix: - include: - - target: core - image: logto - - target: cloud - image: cloud - - runs-on: ubuntu-latest - needs: [dockerize-core, dockerize-cloud] - environment: ${{ inputs.target || 'dev' }}-${{ matrix.target }}-staging - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node and pnpm - if: ${{ (inputs.target || 'dev') == 'dev' && matrix.target == 'core' }} - uses: silverhand-io/actions-node-pnpm-run-steps@v3 - - - name: Deploy database alteration - if: ${{ (inputs.target || 'dev') == 'dev' && matrix.target == 'core' }} - run: | - pnpm prepack - pnpm cli db alt deploy next - env: - DB_URL: ${{ secrets.DB_URL }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: silverhand-bot - password: ${{ secrets.BOT_PAT }} - - - name: Login via Azure CLI - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Deploy ${{ matrix.target }} to containerapp - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ vars.APP_NAME }} - slot-name: staging - images: ghcr.io/logto-io/${{ matrix.image }}:${{ (inputs.target || 'dev') }} - - swap-staging-prod: - strategy: - fail-fast: false - matrix: - target: [core, cloud] - - runs-on: ubuntu-latest - needs: deploy - environment: ${{ inputs.target || 'dev' }}-${{ matrix.target }} - - steps: - - name: Login via Azure CLI - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Swap ${{ matrix.target }} to production - # See https://learn.microsoft.com/en-us/cli/azure/webapp/deployment/slot?view=azure-cli-latest#az-webapp-deployment-slot-swap - run: az webapp deployment slot swap -g ${{ vars.RESOURCE_GROUP }} -n ${{ vars.APP_NAME }} --slot staging - + # Publish packages and create git tags if needed publish-and-tag: runs-on: ubuntu-latest diff --git a/Dockerfile.cloud b/Dockerfile.cloud deleted file mode 100644 index 955931154..000000000 --- a/Dockerfile.cloud +++ /dev/null @@ -1,44 +0,0 @@ -###### [STAGE] Build ###### -FROM node:18-alpine as builder -WORKDIR /etc/logto -ENV CI=true - -# No need for Docker build -ENV PUPPETEER_SKIP_DOWNLOAD=true - -### Install toolchain ### -RUN npm add --location=global pnpm@^8.0.0 -# https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#node-gyp-alpine -RUN apk add --no-cache python3 make g++ - -COPY . . - -### Install dependencies ### -RUN node .scripts/update-parcelrc.js -RUN pnpm i - -### Build ### -# Admin Console build env -ENV CONSOLE_PUBLIC_URL=/ -ENV IS_CLOUD=1 -ARG applicationinsights_connection_string -ENV APPLICATIONINSIGHTS_CONNECTION_STRING=${applicationinsights_connection_string} -RUN pnpm prepack -RUN pnpm connectors:build && pnpm -r --filter @logto/console --filter @logto/cloud build - -### Add official connectors ### -RUN pnpm cli connector link -p . - -### Prune dependencies for production ### -RUN rm -rf node_modules packages/**/node_modules -RUN NODE_ENV=production pnpm i - -### Clean up ### -RUN rm -rf .scripts .parcel-cache pnpm-*.yaml - -###### [STAGE] Seal ###### -FROM node:18-alpine as app -WORKDIR /etc/logto-cloud -COPY --from=builder /etc/logto . -EXPOSE 3003 -ENTRYPOINT ["npm", "run", "start:cloud"] diff --git a/LICENSE b/LICENSE index 14c44cd76..a612ad981 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,3 @@ -Portions of this software are licensed as follows: - -* All content that resides under the "packages/cloud" directory of this repository, if that directory exists, is licensed under the license defined in "packages/cloud/LICENSE" (Elastic-2.0). -* All third party components incorporated into this software are licensed under the original license provided by the owner of the applicable component. -* Content outside of the above mentioned directories or restrictions above is available under the "MPL-2.0" license as defined below. - Mozilla Public License Version 2.0 ================================== diff --git a/package.json b/package.json index 796b42a78..519ada908 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,8 @@ "prepare": "if test \"$NODE_ENV\" != \"production\" && test \"$CI\" != \"true\" ; then husky install ; fi", "prepack": "pnpm -r prepack", "dev": "pnpm -r prepack && pnpm start:dev", - "dev:cloud": "IS_CLOUD=1 pnpm -r prepack && pnpm start:dev:cloud", - "start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" --filter=!@logto/cloud dev", - "start:dev:cloud": "CONSOLE_PUBLIC_URL=/ IS_CLOUD=1 pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" dev", + "start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter \"!./packages/connectors/connector-*\" dev", "start": "cd packages/core && NODE_ENV=production node .", - "start:cloud": "cd packages/cloud && NODE_ENV=production node .", "cli": "logto", "alteration": "logto db alt", "connectors:build": "pnpm -r --filter \"./packages/connectors/connector-*\" build", diff --git a/packages/cloud/CHANGELOG.md b/packages/cloud/CHANGELOG.md deleted file mode 100644 index 72327de6b..000000000 --- a/packages/cloud/CHANGELOG.md +++ /dev/null @@ -1,121 +0,0 @@ -# @logto/cloud - -## 0.2.6 - -### Patch Changes - -- ac65c8de4: ### Enable strict CSP policy check header - - This change removes the report only flag from CSP security header settings, which will enables the strict CSP policy check for all requests. - -- Updated dependencies [2cab3787c] -- Updated dependencies [73666f8fa] -- Updated dependencies [268dc50e7] -- Updated dependencies [fa0dbafe8] -- Updated dependencies [497d5b526] - - @logto/schemas@1.5.0 - - @logto/cli@1.5.0 - -## 0.2.5 - -### Patch Changes - -- Updated dependencies [3c84d81ff] -- Updated dependencies [457cb2822] -- Updated dependencies [736d6d212] -- Updated dependencies [4945b0be2] -- Updated dependencies [30033421c] -- Updated dependencies [91906f0eb] - - @logto/cli@1.2.0 - - @logto/schemas@1.2.0 - - @logto/shared@2.0.0 - - @logto/core-kit@2.0.0 - - @logto/connector-kit@1.1.1 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies [e2ec1f93e] - - @logto/cli@1.1.0 - - @logto/schemas@1.1.0 - - @logto/shared@1.0.3 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies [5b4da1e3d] - - @logto/schemas@1.0.7 - - @logto/cli@1.0.3 - - @logto/shared@1.0.2 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies [621b09ba1] - - @logto/schemas@1.0.1 - - @logto/cli@1.0.2 - - @logto/shared@1.0.1 - -## 0.2.1 - -### Patch Changes - -- @logto/cli@1.0.1 - -## 0.2.0 - -### Minor Changes - -- 343b1090f: Add demo social connectors for new tenant -- 343b1090f: Allow admin tenant admin to create tenants without limitation -- 343b1090f: Add send sms service -- 343b1090f: Add Cloud API: send email - -### Patch Changes - -- 343b1090f: **Seed data for cloud** - - - cli!: remove `oidc` option for `database seed` command as it's unused - - cli: add hidden `--cloud` option for `database seed` command to init cloud data - - cli, cloud: appending Redirect URIs to Admin Console will deduplicate values before update - - move `UrlSet` and `GlobalValues` to `@logto/shared` - -- Updated dependencies [343b1090f] -- Updated dependencies [f41fd3f05] -- Updated dependencies [e63f5f8b0] -- Updated dependencies [f41fd3f05] -- Updated dependencies [343b1090f] -- Updated dependencies [343b1090f] -- Updated dependencies [c12717412] -- Updated dependencies [343b1090f] -- Updated dependencies [38970fb88] -- Updated dependencies [343b1090f] -- Updated dependencies [343b1090f] -- Updated dependencies [343b1090f] -- Updated dependencies [343b1090f] -- Updated dependencies [1c9160112] -- Updated dependencies [1c9160112] -- Updated dependencies [f41fd3f05] -- Updated dependencies [7fb689b73] -- Updated dependencies [1c9160112] -- Updated dependencies [343b1090f] -- Updated dependencies [f41fd3f05] -- Updated dependencies [f41fd3f05] -- Updated dependencies [2d45cc3e6] -- Updated dependencies [3ff2e90cd] - - @logto/schemas@1.0.0 - - @logto/shared@1.0.0 - - @logto/cli@1.0.0 - - @logto/connector-kit@1.1.0 - - @logto/core-kit@1.1.0 - -## 0.1.1-rc.0 - -### Patch Changes - -- Updated dependencies [c12717412] - - @logto/schemas@1.0.0-rc.1 - - @logto/shared@1.0.0-rc.1 diff --git a/packages/cloud/LICENSE b/packages/cloud/LICENSE deleted file mode 100644 index 809108b85..000000000 --- a/packages/cloud/LICENSE +++ /dev/null @@ -1,93 +0,0 @@ -Elastic License 2.0 - -URL: https://www.elastic.co/licensing/elastic-license - -## Acceptance - -By using the software, you agree to all of the terms and conditions below. - -## Copyright License - -The licensor grants you a non-exclusive, royalty-free, worldwide, -non-sublicensable, non-transferable license to use, copy, distribute, make -available, and prepare derivative works of the software, in each case subject to -the limitations and conditions below. - -## Limitations - -You may not provide the software to third parties as a hosted or managed -service, where the service provides users with access to any substantial set of -the features or functionality of the software. - -You may not move, change, disable, or circumvent the license key functionality -in the software, and you may not remove or obscure any functionality in the -software that is protected by the license key. - -You may not alter, remove, or obscure any licensing, copyright, or other notices -of the licensor in the software. Any use of the licensor’s trademarks is subject -to applicable law. - -## Patents - -The licensor grants you a license, under any patent claims the licensor can -license, or becomes able to license, to make, have made, use, sell, offer for -sale, import and have imported the software, in each case subject to the -limitations and conditions in this license. This license does not cover any -patent claims that you cause to be infringed by modifications or additions to -the software. If you or your company make any written claim that the software -infringes or contributes to infringement of any patent, your patent license for -the software granted under these terms ends immediately. If your company makes -such a claim, your patent license ends immediately for work on behalf of your -company. - -## Notices - -You must ensure that anyone who gets a copy of any part of the software from you -also gets a copy of these terms. - -If you modify the software, you must include in any modified copies of the -software prominent notices stating that you have modified the software. - -## No Other Rights - -These terms do not imply any licenses other than those expressly granted in -these terms. - -## Termination - -If you use the software in violation of these terms, such use is not licensed, -and your licenses will automatically terminate. If the licensor provides you -with a notice of your violation, and you cease all violation of this license no -later than 30 days after you receive that notice, your licenses will be -reinstated retroactively. However, if you violate these terms after such -reinstatement, any additional violation of these terms will cause your licenses -to terminate automatically and permanently. - -## No Liability - -*As far as the law allows, the software comes as is, without any warranty or -condition, and the licensor will not be liable to you for any damages arising -out of these terms or the use or nature of the software, under any kind of -legal claim.* - -## Definitions - -The **licensor** is the entity offering these terms, and the **software** is the -software the licensor makes available under these terms, including any portion -of it. - -**you** refers to the individual or entity agreeing to these terms. - -**your company** is any legal entity, sole proprietorship, or other kind of -organization that you work for, plus all organizations that have control over, -are under the control of, or are under common control with that -organization. **control** means ownership of substantially all the assets of an -entity, or the power to direct its management and policies by vote, contract, or -otherwise. Control can be direct or indirect. - -**your licenses** are all the licenses granted to you for the software under -these terms. - -**use** means anything you do with the software requiring one of your licenses. - -**trademark** means trademarks, service marks, and similar rights. diff --git a/packages/cloud/jest.config.js b/packages/cloud/jest.config.js deleted file mode 100644 index 2b18a6c22..000000000 --- a/packages/cloud/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import baseConfig from '@silverhand/jest-config'; - -/** @type {import('jest').Config} */ -const config = { - ...baseConfig, - coveragePathIgnorePatterns: ['/node_modules/', '/test-utils/'], - collectCoverageFrom: ['**/*.js'], - roots: ['./build'], - setupFilesAfterEnv: ['./jest.setup.js'], -}; -export default config; diff --git a/packages/cloud/jest.setup.js b/packages/cloud/jest.setup.js deleted file mode 100644 index e42238ff8..000000000 --- a/packages/cloud/jest.setup.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Setup environment variables for unit test - */ - -process.env.DB_URL = 'postgres://mock.db.url'; -process.env.ENDPOINT = 'https://logto.test'; -process.env.NODE_ENV = 'test'; diff --git a/packages/cloud/nodemon.json b/packages/cloud/nodemon.json deleted file mode 100644 index 0a6979406..000000000 --- a/packages/cloud/nodemon.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "exec": "tsc -p tsconfig.build.json --incremental && node ./build/index.js || exit 1", - "ignore": [ - "node_modules/**/node_modules", - "../integration-tests/" - ], - "watch": [ - "./src/", - "../core/src/", - "./node_modules/", - "../../.env" - ], - "ext": "json,js,jsx,ts,tsx", - "delay": 1000 -} diff --git a/packages/cloud/package.json b/packages/cloud/package.json deleted file mode 100644 index 579ea7311..000000000 --- a/packages/cloud/package.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "@logto/cloud", - "version": "0.2.6", - "description": "Logto Cloud service.", - "main": "build/index.js", - "author": "Silverhand Inc. ", - "license": "Elastic-2.0", - "type": "module", - "private": true, - "exports": { - "./routes": "./build/routes/index.js" - }, - "imports": { - "#src/*": "./build/*" - }, - "scripts": { - "precommit": "lint-staged", - "build": "rm -rf build/ && tsc -p tsconfig.build.json", - "build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap", - "lint": "eslint --ext .ts src", - "lint:report": "pnpm lint --format json --output-file report.json", - "dev": "rm -rf build/ && nodemon", - "start": "NODE_ENV=production node .", - "prepack": "pnpm build", - "test:only": "NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" jest --logHeapUsage", - "test": "pnpm build:test && pnpm test:only", - "test:ci": "pnpm test:only --coverage --silent" - }, - "dependencies": { - "@logto/app-insights": "workspace:^1.0.0", - "@logto/cli": "workspace:^1.5.0", - "@logto/connector-kit": "workspace:^1.1.1", - "@logto/core-kit": "workspace:^2.0.0", - "@logto/schemas": "workspace:^1.5.0", - "@logto/shared": "workspace:^2.0.0", - "@silverhand/essentials": "^2.5.0", - "@withtyped/postgres": "^0.12.0", - "@withtyped/server": "^0.12.0", - "accepts": "^1.3.8", - "chalk": "^5.0.0", - "decamelize": "^6.0.0", - "dotenv": "^16.0.0", - "fetch-retry": "^5.0.4", - "find-up": "^6.3.0", - "helmet": "^7.0.0", - "http-proxy": "^1.18.1", - "jose": "^4.11.0", - "mime-types": "^2.1.35", - "zod": "^3.20.2" - }, - "devDependencies": { - "@silverhand/eslint-config": "3.0.1", - "@silverhand/jest-config": "3.0.0", - "@silverhand/ts-config": "3.0.0", - "@types/accepts": "^1.3.5", - "@types/http-proxy": "^1.17.9", - "@types/jest": "^29.4.0", - "@types/mime-types": "^2.1.1", - "@types/node": "^18.11.18", - "eslint": "^8.21.0", - "jest": "^29.5.0", - "lint-staged": "^13.0.0", - "nodemon": "^2.0.19", - "prettier": "^2.8.1", - "typescript": "^5.0.0" - }, - "engines": { - "node": "^18.12.0" - }, - "eslintConfig": { - "extends": "@silverhand" - }, - "prettier": "@silverhand/eslint-config/.prettierrc" -} diff --git a/packages/cloud/src/env-set/index.ts b/packages/cloud/src/env-set/index.ts deleted file mode 100644 index f7f228f3f..000000000 --- a/packages/cloud/src/env-set/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { GlobalValues } from '@logto/shared'; - -export const EnvSet = { - global: new GlobalValues(), - - get isProduction() { - return this.global.isProduction; - }, -}; diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts deleted file mode 100644 index 130dcb26e..000000000 --- a/packages/cloud/src/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { cloudApiIndicator } from '@logto/schemas'; -import type { RequestContext } from '@withtyped/server'; -import createServer, { withBody, compose, withRequest } from '@withtyped/server'; -import dotenv from 'dotenv'; -import { findUp } from 'find-up'; - -dotenv.config({ path: await findUp('.env', {}) }); - -const { appInsights } = await import('@logto/app-insights/node'); - -if (await appInsights.setup('cloud')) { - console.debug('Initialized ApplicationInsights'); -} - -const { default: withAuth } = await import('./middleware/with-auth.js'); -const { default: withHttpProxy } = await import('./middleware/with-http-proxy.js'); -const { default: withPathname } = await import('./middleware/with-pathname.js'); -const { default: withSpa } = await import('./middleware/with-spa.js'); -const { default: withErrorReport } = await import('./middleware/with-error-report.js'); -const { default: withSecurityHeaders } = await import('./middleware/with-security-headers.js'); - -const { EnvSet } = await import('./env-set/index.js'); -const { default: router } = await import('./routes/index.js'); -const { default: anonymousRouter } = await import('./routes-anonymous/index.js'); - -const ignorePathnames = ['/api']; - -const { listen } = createServer({ - port: 3003, - composer: compose() - .and(withErrorReport()) - .and(withRequest()) - .and(anonymousRouter.routes()) - .and(withSecurityHeaders()) - .and( - withPathname( - '/api', - compose() - .and(withBody()) - .and( - withAuth({ endpoint: EnvSet.global.adminUrlSet.endpoint, audience: cloudApiIndicator }) - ) - .and(router.routes()) - ) - ) - .and( - EnvSet.isProduction - ? withSpa({ pathname: '/', root: '../console/dist', ignorePathnames }) - : withHttpProxy('/', { - target: 'http://localhost:5002', - changeOrigin: true, - ignorePathnames, - }) - ), -}); - -await listen((port) => { - console.log(`Logto cloud is running at http://localhost:${port}`); -}); diff --git a/packages/cloud/src/libraries/services.ts b/packages/cloud/src/libraries/services.ts deleted file mode 100644 index ae29c31e9..000000000 --- a/packages/cloud/src/libraries/services.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js'; -import type { - AllConnector, - EmailConnector, - SendMessagePayload, - ConnectorType, - SmsConnector, -} from '@logto/connector-kit'; -import { validateConfig } from '@logto/connector-kit'; -import type { ServiceLogType } from '@logto/schemas'; -import { adminTenantId } from '@logto/schemas'; -import { generateStandardId } from '@logto/shared'; -import { trySafe } from '@silverhand/essentials'; -import { RequestError } from '@withtyped/server'; - -import type { Queries } from '#src/queries/index.js'; -import type { LogtoConnector } from '#src/utils/connector/index.js'; -import { loadConnectorFactories } from '#src/utils/connector/index.js'; -import { jsonObjectGuard } from '#src/utils/guard.js'; - -export const serviceCountLimitForTenant = 100; - -export class ServicesLibrary { - constructor(public readonly queries: Queries) {} - - async getTenantIdFromApplicationId(applicationId: string) { - const application = await this.queries.applications.findApplicationById( - applicationId, - adminTenantId - ); - - return application.customClientMetadata.tenantId; - } - - async getAdminTenantLogtoConnectors(): Promise { - const databaseConnectors = await this.queries.connectors.findAllConnectors(adminTenantId); - - const logtoConnectors = await Promise.all( - databaseConnectors.map(async (databaseConnector) => { - const { id, metadata, connectorId } = databaseConnector; - - const connectorFactories = await loadConnectorFactories(); - - const connectorFactory = connectorFactories.find( - ({ metadata }) => metadata.id === connectorId - ); - - if (!connectorFactory) { - return; - } - - return trySafe(async () => { - const { rawConnector, rawMetadata } = await buildRawConnector( - connectorFactory, - async () => { - const databaseConnectors = await this.queries.connectors.findAllConnectors( - adminTenantId - ); - const connector = databaseConnectors.find((connector) => connector.id === id); - - if (!connector) { - throw new RequestError(`Unable to find connector ${id}`, 500); - } - - return connector.config; - } - ); - - const connector: AllConnector = { - ...defaultConnectorMethods, - ...rawConnector, - metadata: { - ...rawMetadata, - ...metadata, - }, - }; - - return { - ...connector, - validateConfig: (config: unknown) => { - validateConfig(config, rawConnector.configGuard); - }, - dbEntry: databaseConnector, - }; - }); - }) - ); - - return logtoConnectors.filter( - (logtoConnector): logtoConnector is LogtoConnector => logtoConnector !== undefined - ); - } - - async sendMessage(type: ConnectorType.Email | ConnectorType.Sms, data: SendMessagePayload) { - const connectors = await this.getAdminTenantLogtoConnectors(); - - const connector = connectors.find( - (connector): connector is LogtoConnector => - connector.type === type - ); - - if (!connector) { - throw new RequestError(`Unable to find ${type} connector`, 500); - } - - const { sendMessage } = connector; - - return sendMessage(data); - } - - async addLog(tenantId: string, type: ServiceLogType, payload?: unknown) { - return this.queries.serviceLogs.insertLog({ - id: generateStandardId(), - type, - tenantId, - payload: trySafe(() => jsonObjectGuard.parse(payload)), - }); - } - - async getTenantBalanceForType(tenantId: string, type: ServiceLogType) { - const usedCount = await this.queries.serviceLogs.countTenantLogs(tenantId, type); - - return serviceCountLimitForTenant - usedCount; - } -} diff --git a/packages/cloud/src/libraries/tenants.ts b/packages/cloud/src/libraries/tenants.ts deleted file mode 100644 index 9a4ec4af3..000000000 --- a/packages/cloud/src/libraries/tenants.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { - generateOidcCookieKey, - generateOidcPrivateKey, -} from '@logto/cli/lib/commands/database/utils.js'; -import { DemoConnector, ServiceConnector } from '@logto/connector-kit'; -import { createTenantMetadata } from '@logto/core-kit'; -import { - type LogtoOidcConfigType, - createAdminTenantApplicationRole, - AdminTenantRole, - createTenantMachineToMachineApplication, - LogtoOidcConfigKey, - LogtoConfigs, - SignInExperiences, - createDefaultAdminConsoleConfig, - createDefaultSignInExperience, - adminTenantId, - createAdminData, - createAdminDataInAdminTenant, - getManagementApiResourceIndicator, - type TenantModel, -} from '@logto/schemas'; -import type { TenantInfo } from '@logto/schemas/models'; -import { generateStandardId } from '@logto/shared'; -import { appendPath } from '@silverhand/essentials'; - -import { EnvSet } from '#src/env-set/index.js'; -import { createApplicationsQueries } from '#src/queries/application.js'; -import { createConnectorsQuery } from '#src/queries/connector.js'; -import type { Queries } from '#src/queries/index.js'; -import { createRolesQuery } from '#src/queries/roles.js'; -import { createSystemsQuery } from '#src/queries/system.js'; -import { createTenantsQueries } from '#src/queries/tenants.js'; -import { createUsersQueries } from '#src/queries/users.js'; -import { getDatabaseName } from '#src/queries/utils.js'; -import { createCloudServiceConnector } from '#src/utils/connector/seed.js'; -import { insertInto } from '#src/utils/query.js'; -import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js'; - -const demoSocialConnectorId = 'logto-social-demo'; - -const oidcConfigBuilders: { - [key in LogtoOidcConfigKey]: () => Promise; -} = { - [LogtoOidcConfigKey.CookieKeys]: async () => [generateOidcCookieKey()], - [LogtoOidcConfigKey.PrivateKeys]: async () => [await generateOidcPrivateKey()], -}; - -export class TenantsLibrary { - constructor(public readonly queries: Queries) {} - - async getAvailableTenants(userId: string): Promise { - const { getManagementApiLikeIndicatorsForUser, getTenantsByIds } = this.queries.tenants; - const { rows } = await getManagementApiLikeIndicatorsForUser(userId); - - const tenantIds = rows - .map(({ indicator }) => getTenantIdFromManagementApiIndicator(indicator)) - .filter((id): id is string => typeof id === 'string'); - - if (tenantIds.length === 0) { - return []; - } - - const rawTenantInfos = await getTenantsByIds(tenantIds); - return rawTenantInfos.map(({ id, name, tag }) => ({ - id, - name, - tag, - indicator: getManagementApiResourceIndicator(id), - })); - } - - async updateTenantById( - tenantId: string, - payload: Partial> - ): Promise { - const { id, name, tag } = await this.queries.tenants.updateTenantById(tenantId, payload); - - return { id, name, tag, indicator: getManagementApiResourceIndicator(id) }; - } - - async deleteTenantById(tenantId: string) { - const { - deleteTenantById, - deleteDatabaseRoleForTenant, - deleteClientTenantManagementApiResourceByTenantId, - deleteClientTenantRoleById, - getTenantById, - deleteClientTenantManagementApplicationById, - removeUrisFromAdminConsoleRedirectUris, - } = this.queries.tenants; - const { cloudUrlSet } = EnvSet.global; - - /** `dbUser` is defined as nullable but we always specified this value when creating a new tenant. */ - const { dbUser } = await getTenantById(tenantId); - if (dbUser) { - /** DB role for building connection for the current tenant. */ - await deleteDatabaseRoleForTenant(dbUser); - } - - await deleteTenantById(tenantId); - - /** - * All applications, resources, scopes and roles attached to the current tenant - * will be deleted per DB design. - * Need to manually delete following applications, roles, resources since they - * are created for admin tenant which will not be deleted automatically. - */ - /** Delete management API for the current tenant. */ - /** `scopes` will be automatically deleted if its related resources have been removed. */ - await deleteClientTenantManagementApiResourceByTenantId(tenantId); - /** Delete admin tenant admin role for the current tenant. */ - await deleteClientTenantRoleById(tenantId); - /** Delete M2M application for the current principal (tenant, characterized by `tenantId`). */ - await deleteClientTenantManagementApplicationById(tenantId); - - await removeUrisFromAdminConsoleRedirectUris( - ...cloudUrlSet.deduplicated().map((url) => appendPath(url, tenantId, 'callback')) - ); - } - - async createNewTenant( - forUserId: string, - payload: Partial> - ): Promise { - const databaseName = await getDatabaseName(this.queries.client); - const { id: tenantId, parentRole, role, password } = createTenantMetadata(databaseName); - - // Init tenant - const createTenant = { - id: tenantId, - dbUser: role, - dbUserPassword: password, - ...payload, - }; - const transaction = await this.queries.client.transaction(); - const tenants = createTenantsQueries(transaction); - const users = createUsersQueries(transaction); - const applications = createApplicationsQueries(transaction); - const roles = createRolesQuery(transaction); - const connectors = createConnectorsQuery(transaction); - const systems = createSystemsQuery(transaction); - - /* === Start === */ - await transaction.start(); - - // Init tenant - await tenants.insertTenant(createTenant); - const insertedTenant = await tenants.getTenantById(tenantId); - await tenants.createTenantRole(parentRole, role, password); - - // Create admin data set (resource, roles, etc.) - const adminDataInAdminTenant = createAdminDataInAdminTenant(tenantId); - await tenants.insertAdminData(adminDataInAdminTenant); - await tenants.insertAdminData(createAdminData(tenantId)); - await users.assignRoleToUser({ - id: generateStandardId(), - tenantId: adminTenantId, - userId: forUserId, - roleId: adminDataInAdminTenant.role.id, - }); - // Create M2M App - const m2mRoleId = await roles.findRoleIdByName( - AdminTenantRole.TenantApplication, - adminTenantId - ); - const m2mApplication = createTenantMachineToMachineApplication(tenantId); - await applications.insertApplication(m2mApplication); - await applications.assignRoleToApplication( - createAdminTenantApplicationRole(m2mApplication.id, m2mRoleId) - ); - - // Create initial configs - await Promise.all([ - ...Object.entries(oidcConfigBuilders).map(async ([key, build]) => - transaction.query(insertInto({ tenantId, key, value: await build() }, LogtoConfigs.table)) - ), - transaction.query(insertInto(createDefaultAdminConsoleConfig(tenantId), LogtoConfigs.table)), - transaction.query( - insertInto(createDefaultSignInExperience(tenantId, true), SignInExperiences.table) - ), - ]); - - // Create demo connectors - const { cloudUrlSet } = EnvSet.global; - - await Promise.all( - [ServiceConnector.Email, DemoConnector.Sms].map(async (connectorId) => { - return connectors.insertConnector( - createCloudServiceConnector({ - tenantId, - connectorId, - appId: m2mApplication.id, - appSecret: m2mApplication.secret, - }) - ); - }) - ); - - // Create demo social connectors - const presetSocialConnectors = await systems.getDemoSocialValue(); - - if (presetSocialConnectors) { - await Promise.all( - presetSocialConnectors.map(async (connector) => { - return connectors.insertConnector({ - id: generateStandardId(), - tenantId, - connectorId: demoSocialConnectorId, - metadata: { - name: { en: connector.name }, - target: connector.provider, - logo: connector.logo, - logoDark: connector.logoDark, - }, - config: { - provider: connector.provider, - clientId: connector.clientId, - redirectUri: `${cloudUrlSet.endpoint.toString()}social-demo-callback`, - }, - }); - }) - ); - } - - // Update Redirect URI for Admin Console - await tenants.appendAdminConsoleRedirectUris( - ...cloudUrlSet.deduplicated().map((url) => appendPath(url, createTenant.id, 'callback')) - ); - - await transaction.end(); - /* === End === */ - - return { - id: tenantId, - name: insertedTenant.name, - tag: insertedTenant.tag, - indicator: adminDataInAdminTenant.resource.indicator, - }; - } -} diff --git a/packages/cloud/src/middleware/with-auth.ts b/packages/cloud/src/middleware/with-auth.ts deleted file mode 100644 index 8ea27b36d..000000000 --- a/packages/cloud/src/middleware/with-auth.ts +++ /dev/null @@ -1,96 +0,0 @@ -import assert from 'node:assert'; -import type { IncomingHttpHeaders } from 'node:http'; - -import { appendPath, tryThat } from '@silverhand/essentials'; -import type { NextFunction, RequestContext } from '@withtyped/server'; -import { RequestError } from '@withtyped/server'; -import fetchRetry from 'fetch-retry'; -import { createRemoteJWKSet, jwtVerify } from 'jose'; -import { z } from 'zod'; - -import { EnvSet } from '#src/env-set/index.js'; - -const bearerTokenIdentifier = 'Bearer'; - -export const extractBearerTokenFromHeaders = ({ authorization }: IncomingHttpHeaders) => { - assert(authorization, new RequestError('Authorization header is missing.', 401)); - assert( - authorization.startsWith(bearerTokenIdentifier), - new RequestError( - `Authorization token type is not supported. Valid type: "${bearerTokenIdentifier}".`, - 401 - ) - ); - - return authorization.slice(bearerTokenIdentifier.length + 1); -}; - -export type WithAuthContext = Context & { - auth: { - id: string; - scopes: string[]; - }; -}; - -export type WithAuthConfig = { - /** The Logto admin tenant endpoint. */ - endpoint: URL; - /** The audience (i.e. Resource Indicator) to expect. */ - audience: string; - /** The scopes (i.e. permissions) to expect. */ - scopes?: string[]; -}; - -export default function withAuth({ - endpoint, - audience, - scopes: expectScopes = [], -}: WithAuthConfig) { - const fetch = fetchRetry(global.fetch); - const getJwkSet = (async () => { - const fetched = await fetch( - new Request(appendPath(endpoint, 'oidc/.well-known/openid-configuration')), - { retries: 5, retryDelay: (attempt) => 2 ** attempt * 1000 } - ); - const { jwks_uri: jwksUri, issuer } = z - .object({ jwks_uri: z.string(), issuer: z.string() }) - .parse(await fetched.json()); - - return Object.freeze([createRemoteJWKSet(new URL(jwksUri)), issuer] as const); - })(); - - return async (context: InputContext, next: NextFunction>) => { - const userId = context.request.headers['development-user-id']?.toString(); - - if (!EnvSet.isProduction && userId) { - console.log(`Found dev user ID ${userId}, skip token validation.`); - - return next({ ...context, auth: { id: userId, scopes: expectScopes } }); - } - - const [getKey, issuer] = await getJwkSet; - - const { - payload: { sub, scope }, - } = await tryThat( - jwtVerify(extractBearerTokenFromHeaders(context.request.headers), getKey, { - issuer, - audience, - }), - (error) => { - console.error(error); - throw new RequestError('JWT verification failed.', 401); - } - ); - - assert(sub, new RequestError('"sub" is missing in JWT.', 401)); - - const scopes = typeof scope === 'string' ? scope.split(' ') : []; - assert( - expectScopes.every((scope) => scopes.includes(scope)), - new RequestError('Forbidden. Please check your permissions.', 403) - ); - - return next({ ...context, auth: { id: sub, scopes } }); - }; -} diff --git a/packages/cloud/src/middleware/with-error-report.ts b/packages/cloud/src/middleware/with-error-report.ts deleted file mode 100644 index f2e6ced3f..000000000 --- a/packages/cloud/src/middleware/with-error-report.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { appInsights } from '@logto/app-insights/node'; -import { tryThat } from '@silverhand/essentials'; -import type { BaseContext, NextFunction } from '@withtyped/server'; - -/** - * Build a middleware function that reports error to Azure Application Insights. - */ -export default function withErrorReport() { - return async (context: InputContext, next: NextFunction) => { - await tryThat(next(context), (error) => { - void appInsights.trackException(error); - throw error; - }); - }; -} diff --git a/packages/cloud/src/middleware/with-http-proxy.ts b/packages/cloud/src/middleware/with-http-proxy.ts deleted file mode 100644 index 596aaa456..000000000 --- a/packages/cloud/src/middleware/with-http-proxy.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { HttpContext, NextFunction, RequestContext } from '@withtyped/server'; -import chalk from 'chalk'; -import type { ServerOptions } from 'http-proxy'; -import HttpProxy from 'http-proxy'; - -import { matchPathname } from '#src/utils/url.js'; - -const { createProxy } = HttpProxy; - -export type WithHttpProxyOptions = ServerOptions & { - /** An array of pathname prefixes to ignore. */ - ignorePathnames?: string[]; -}; - -export default function withHttpProxy( - pathname: string, - { ignorePathnames, ...options }: WithHttpProxyOptions -) { - const proxy = createProxy(options); - - proxy.on('start', (request, __, target) => { - console.log( - `\t${chalk.italic(chalk.gray('proxy ->'))}`, - new URL(request.url ?? '/', typeof target === 'object' ? target.href : target).toString() - ); - }); - - return async ( - context: InputContext, - next: NextFunction, - { request, response }: HttpContext - ) => { - const { - request: { url }, - } = context; - - const matched = matchPathname(pathname, url.pathname, ignorePathnames); - - if (!matched) { - return next(context); - } - - await new Promise((resolve) => { - response.once('finish', resolve); - proxy.web(request, response, options); - }); - - return next({ ...context, status: 'ignore' }); - }; -} diff --git a/packages/cloud/src/middleware/with-pathname.ts b/packages/cloud/src/middleware/with-pathname.ts deleted file mode 100644 index b4b710e99..000000000 --- a/packages/cloud/src/middleware/with-pathname.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { - HttpContext, - MiddlewareFunction, - NextFunction, - RequestContext, -} from '@withtyped/server'; - -import { matchPathname } from '#src/utils/url.js'; - -/** - * Build a middleware function that conditionally runs the given middleware function when: - * - * - The current pathname matches the given pathname prefix; and - * - `context.status` is unset (i.e. `undefined`). - * - * @param pathname The pathname prefix to match. - * @param run The middleware function to run with the prefix matches. - */ -export default function withPathname< - InputContext extends RequestContext, - OutputContext extends RequestContext ->(pathname: string, run: MiddlewareFunction) { - return async ( - context: InputContext, - next: NextFunction, - httpContext: HttpContext - ) => { - if (context.status ?? !matchPathname(pathname, context.request.url.pathname)) { - return next(context); - } - - return run(context, next, httpContext); - }; -} diff --git a/packages/cloud/src/middleware/with-security-headers.ts b/packages/cloud/src/middleware/with-security-headers.ts deleted file mode 100644 index a6b92a549..000000000 --- a/packages/cloud/src/middleware/with-security-headers.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'node:http'; -import { promisify } from 'node:util'; - -import { conditionalArray } from '@silverhand/essentials'; -import type { NextFunction, HttpContext, RequestContext } from '@withtyped/server'; -import helmet, { type HelmetOptions } from 'helmet'; - -import { EnvSet } from '#src/env-set/index.js'; - -/** - * Apply security headers to the response using helmet - * @see https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html for recommended headers - * @see https://helmetjs.github.io/ for more details - * @returns middleware - */ - -const helmetPromise = async ( - settings: HelmetOptions, - request: IncomingMessage, - response: ServerResponse -) => - promisify((callback) => { - helmet(settings)(request, response, (error) => { - // Make TS happy - callback(error, null); - }); - })(); - -export default function withSecurityHeaders() { - const { - global: { adminUrlSet, cloudUrlSet, urlSet }, - isProduction, - } = EnvSet; - - const adminOrigins = adminUrlSet.origins; - const cloudOrigins = cloudUrlSet.origins; - const coreOrigins = urlSet.origins; - const developmentOrigins = conditionalArray(!isProduction && 'ws:'); - const appInsightsOrigins = ['https://*.applicationinsights.azure.com']; - // Gtag will load by `