mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(cloud): GET /api/tenants
This commit is contained in:
parent
8fa9e7a4a1
commit
1808a25570
19 changed files with 190 additions and 27 deletions
|
@ -7,7 +7,7 @@ module.exports = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ['@commitlint/config-conventional'],
|
||||||
rules: {
|
rules: {
|
||||||
'type-enum': [2, 'always', [...rules['type-enum'][2], 'api', 'release']],
|
'type-enum': [2, 'always', [...rules['type-enum'][2], 'api', 'release']],
|
||||||
'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'ui', 'deps','cli', 'toolkit']],
|
'scope-enum': [2, 'always', ['connector', 'console', 'core', 'demo-app', 'test', 'phrases', 'schemas', 'shared', 'ui', 'deps','cli', 'toolkit', 'cloud']],
|
||||||
// Slightly increase the tolerance to allow the appending PR number
|
// Slightly increase the tolerance to allow the appending PR number
|
||||||
...(isCi && { 'header-max-length': [2, 'always', 110] })
|
...(isCi && { 'header-max-length': [2, 'always', 110] })
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"dev": "pnpm -r prepack && pnpm start:dev",
|
"dev": "pnpm -r prepack && pnpm start:dev",
|
||||||
"dev:cloud": "pnpm -r prepack && pnpm start:dev:cloud",
|
"dev:cloud": "pnpm -r prepack && pnpm start:dev:cloud",
|
||||||
"start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter=!@logto/cloud dev",
|
"start:dev": "pnpm -r --parallel --filter=!@logto/integration-tests --filter=!@logto/cloud dev",
|
||||||
"start:dev:cloud": "pnpm -r --parallel --filter=!@logto/integration-tests dev",
|
"start:dev:cloud": "CONSOLE_PUBLIC_URL=/ pnpm -r --parallel --filter=!@logto/integration-tests dev",
|
||||||
"start": "cd packages/core && NODE_ENV=production node .",
|
"start": "cd packages/core && NODE_ENV=production node .",
|
||||||
"cli": "logto",
|
"cli": "logto",
|
||||||
"alteration": "logto db alt",
|
"alteration": "logto db alt",
|
||||||
|
|
|
@ -19,11 +19,14 @@
|
||||||
"start": "NODE_ENV=production node build/index.js"
|
"start": "NODE_ENV=production node build/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@logto/schemas": "workspace:*",
|
||||||
"@logto/shared": "workspace:*",
|
"@logto/shared": "workspace:*",
|
||||||
"@silverhand/essentials": "2.2.0",
|
"@silverhand/essentials": "2.2.0",
|
||||||
"@withtyped/postgres": "^0.6.0",
|
"@withtyped/postgres": "^0.6.0",
|
||||||
"@withtyped/server": "^0.6.0",
|
"@withtyped/server": "^0.6.0",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"find-up": "^6.3.0",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
"mime-types": "^2.1.35"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +1,29 @@
|
||||||
import createServer, { compose, withRequest } from '@withtyped/server';
|
import createServer, { compose, withRequest } from '@withtyped/server';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { findUp } from 'find-up';
|
||||||
|
|
||||||
import withHttpProxy from './middleware/with-http-proxy.js';
|
import withHttpProxy from './middleware/with-http-proxy.js';
|
||||||
import withSpa from './middleware/with-spa.js';
|
import withSpa from './middleware/with-spa.js';
|
||||||
|
|
||||||
|
dotenv.config({ path: await findUp('.env', {}) });
|
||||||
|
|
||||||
|
const { default: router } = await import('./routes/index.js');
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
const ignorePathnames = ['/api'];
|
||||||
|
|
||||||
const { listen } = createServer({
|
const { listen } = createServer({
|
||||||
port: 3003,
|
port: 3003,
|
||||||
composer: compose(withRequest()).and(
|
composer: compose(withRequest())
|
||||||
|
.and(router.routes())
|
||||||
|
.and(
|
||||||
isProduction
|
isProduction
|
||||||
? withSpa({ pathname: '/console', root: '../console/dist' })
|
? withSpa({ pathname: '/', root: '../console/dist', ignorePathnames })
|
||||||
: withHttpProxy('/console', { target: 'http://localhost:5002', changeOrigin: true })
|
: withHttpProxy('/', {
|
||||||
|
target: 'http://localhost:5002',
|
||||||
|
changeOrigin: true,
|
||||||
|
ignorePathnames,
|
||||||
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,16 @@ import { matchPathname } from '#src/utils/url.js';
|
||||||
|
|
||||||
const { createProxy } = HttpProxy;
|
const { createProxy } = HttpProxy;
|
||||||
|
|
||||||
|
export type WithHttpProxyOptions = ServerOptions & {
|
||||||
|
/**
|
||||||
|
* An array of pathname prefixes to ignore.
|
||||||
|
*/
|
||||||
|
ignorePathnames?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export default function withHttpProxy<InputContext extends RequestContext>(
|
export default function withHttpProxy<InputContext extends RequestContext>(
|
||||||
pathname: string,
|
pathname: string,
|
||||||
options: ServerOptions
|
{ ignorePathnames, ...options }: WithHttpProxyOptions
|
||||||
) {
|
) {
|
||||||
const proxy = createProxy(options);
|
const proxy = createProxy(options);
|
||||||
|
|
||||||
|
@ -31,7 +38,7 @@ export default function withHttpProxy<InputContext extends RequestContext>(
|
||||||
|
|
||||||
const matched = matchPathname(pathname, url.pathname);
|
const matched = matchPathname(pathname, url.pathname);
|
||||||
|
|
||||||
if (!matched) {
|
if (!matched || ignorePathnames?.some((prefix) => matchPathname(prefix, url.pathname))) {
|
||||||
return next(context);
|
return next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,5 +50,3 @@ export default function withHttpProxy<InputContext extends RequestContext>(
|
||||||
return next({ ...context, status: 'ignore' });
|
return next({ ...context, status: 'ignore' });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { ServerOptions } from 'http-proxy';
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ export type WithSpaConfig = {
|
||||||
* @default '/'
|
* @default '/'
|
||||||
*/
|
*/
|
||||||
pathname?: string;
|
pathname?: string;
|
||||||
|
/**
|
||||||
|
* An array of pathname prefixes to ignore.
|
||||||
|
*/
|
||||||
|
ignorePathnames?: string[];
|
||||||
/**
|
/**
|
||||||
* The path to file to serve when the given path cannot be found in the file system.
|
* The path to file to serve when the given path cannot be found in the file system.
|
||||||
* @default 'index.html'
|
* @default 'index.html'
|
||||||
|
@ -32,6 +36,7 @@ export default function withSpa<InputContext extends RequestContext>({
|
||||||
maxAge = 604_800,
|
maxAge = 604_800,
|
||||||
root,
|
root,
|
||||||
pathname: rootPathname = '/',
|
pathname: rootPathname = '/',
|
||||||
|
ignorePathnames,
|
||||||
indexPath: index = 'index.html',
|
indexPath: index = 'index.html',
|
||||||
}: WithSpaConfig) {
|
}: WithSpaConfig) {
|
||||||
assert(root, new Error('Root directory is required to serve files.'));
|
assert(root, new Error('Root directory is required to serve files.'));
|
||||||
|
@ -44,7 +49,7 @@ export default function withSpa<InputContext extends RequestContext>({
|
||||||
|
|
||||||
const pathname = matchPathname(rootPathname, url.pathname);
|
const pathname = matchPathname(rootPathname, url.pathname);
|
||||||
|
|
||||||
if (!pathname) {
|
if (!pathname || ignorePathnames?.some((prefix) => matchPathname(prefix, url.pathname))) {
|
||||||
return next(context);
|
return next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
packages/cloud/src/queries/index.ts
Normal file
5
packages/cloud/src/queries/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createQueryClient } from '@withtyped/postgres';
|
||||||
|
|
||||||
|
import { parseDsn } from '#src/utils/postgres.js';
|
||||||
|
|
||||||
|
export const client = createQueryClient(parseDsn(process.env.DB_URL));
|
24
packages/cloud/src/queries/tenants.ts
Normal file
24
packages/cloud/src/queries/tenants.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { adminTenantId, getManagementApiResourceIndicator, PredefinedScope } from '@logto/schemas';
|
||||||
|
import type { PostgresQueryClient } from '@withtyped/postgres';
|
||||||
|
import { sql } from '@withtyped/postgres';
|
||||||
|
|
||||||
|
export const createTenantsQueries = (client: PostgresQueryClient) => {
|
||||||
|
const getManagementApiLikeIndicatorsForUser = async (userId: string) =>
|
||||||
|
client.query<{ indicator: string }>(sql`
|
||||||
|
select resources.indicator from roles
|
||||||
|
join users_roles
|
||||||
|
on users_roles.role_id = roles.id
|
||||||
|
and users_roles.user_id = ${userId}
|
||||||
|
join roles_scopes
|
||||||
|
on roles.id = roles_scopes.role_id
|
||||||
|
join scopes
|
||||||
|
on roles_scopes.scope_id = scopes.id
|
||||||
|
and scopes.name = ${PredefinedScope.All}
|
||||||
|
join resources
|
||||||
|
on scopes.resource_id = resources.id
|
||||||
|
and resources.indicator like ${getManagementApiResourceIndicator('%')}
|
||||||
|
where roles.tenant_id = ${adminTenantId};
|
||||||
|
`);
|
||||||
|
|
||||||
|
return { getManagementApiLikeIndicatorsForUser };
|
||||||
|
};
|
7
packages/cloud/src/routes/index.ts
Normal file
7
packages/cloud/src/routes/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { createRouter } from '@withtyped/server';
|
||||||
|
|
||||||
|
import { tenants } from './tenants.js';
|
||||||
|
|
||||||
|
const router = createRouter('/api').pack(tenants);
|
||||||
|
|
||||||
|
export default router;
|
20
packages/cloud/src/routes/tenants.ts
Normal file
20
packages/cloud/src/routes/tenants.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Router } from '@withtyped/server';
|
||||||
|
|
||||||
|
import { client } from '#src/queries/index.js';
|
||||||
|
import { createTenantsQueries } from '#src/queries/tenants.js';
|
||||||
|
import { getTenantIdFromManagementApiIndicator } from '#src/utils/tenant.js';
|
||||||
|
|
||||||
|
const { getManagementApiLikeIndicatorsForUser } = createTenantsQueries(client);
|
||||||
|
|
||||||
|
export const tenants = new Router('/tenants').get('/', {}, async (context, next) => {
|
||||||
|
const { rows } = await getManagementApiLikeIndicatorsForUser('some_user_id');
|
||||||
|
|
||||||
|
const tenants = rows
|
||||||
|
.map(({ indicator }) => ({
|
||||||
|
id: getTenantIdFromManagementApiIndicator(indicator),
|
||||||
|
indicator,
|
||||||
|
}))
|
||||||
|
.filter((tenant): tenant is { id: string; indicator: string } => Boolean(tenant.id));
|
||||||
|
|
||||||
|
return next({ ...context, json: tenants });
|
||||||
|
});
|
63
packages/cloud/src/utils/postgres.ts
Normal file
63
packages/cloud/src/utils/postgres.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import type { createQueryClient } from '@withtyped/postgres';
|
||||||
|
|
||||||
|
type CreateClientConfig = Parameters<typeof createQueryClient>[0];
|
||||||
|
|
||||||
|
/* eslint-disable @silverhand/fp/no-mutation */
|
||||||
|
// Edited from https://github.com/gajus/slonik/blob/d66b76c44638c8b424cea55475d6e1385c2caae8/src/utilities/parseDsn.ts
|
||||||
|
export const parseDsn = (dsn?: string): CreateClientConfig => {
|
||||||
|
if (!dsn?.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(dsn);
|
||||||
|
|
||||||
|
const config: NonNullable<CreateClientConfig> = {};
|
||||||
|
|
||||||
|
if (url.host) {
|
||||||
|
config.host = decodeURIComponent(url.hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.port) {
|
||||||
|
config.port = Number(url.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = url.pathname.split('/')[1];
|
||||||
|
|
||||||
|
if (database) {
|
||||||
|
config.database = decodeURIComponent(database);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.username) {
|
||||||
|
config.user = decodeURIComponent(url.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.password) {
|
||||||
|
config.password = decodeURIComponent(url.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
application_name: applicationName,
|
||||||
|
sslmode: sslMode,
|
||||||
|
...unsupportedOptions
|
||||||
|
} = Object.fromEntries(url.searchParams);
|
||||||
|
|
||||||
|
if (Object.keys(unsupportedOptions).length > 0) {
|
||||||
|
console.warn(
|
||||||
|
{
|
||||||
|
unsupportedOptions,
|
||||||
|
},
|
||||||
|
'unsupported DSN parameters'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationName) {
|
||||||
|
config.application_name = applicationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sslMode === 'require') {
|
||||||
|
config.ssl = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
/* eslint-enable @silverhand/fp/no-mutation */
|
9
packages/cloud/src/utils/tenant.ts
Normal file
9
packages/cloud/src/utils/tenant.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { getManagementApiResourceIndicator } from '@logto/schemas';
|
||||||
|
import { trySafe } from '@silverhand/essentials';
|
||||||
|
|
||||||
|
export const getTenantIdFromManagementApiIndicator = (indicator: string) => {
|
||||||
|
const toMatch = '^' + getManagementApiResourceIndicator('([^.]*)') + '$';
|
||||||
|
const url = trySafe(() => new URL(indicator));
|
||||||
|
|
||||||
|
return url && new RegExp(toMatch).exec(url.href)?.[1];
|
||||||
|
};
|
|
@ -14,6 +14,10 @@ export const matchPathname = (toMatch: string, pathname: string) => {
|
||||||
return '/';
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toMatchPathname === '/') {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
if (normalized.startsWith(toMatchPathname + '/')) {
|
if (normalized.startsWith(toMatchPathname + '/')) {
|
||||||
return normalized.slice(toMatchPathname.length);
|
return normalized.slice(toMatchPathname.length);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"start": "parcel src/index.html",
|
"start": "parcel src/index.html",
|
||||||
"dev": "cross-env PORT=5002 parcel src/index.html --public-url /console --no-cache --hmr-port 6002",
|
"dev": "cross-env PORT=5002 parcel src/index.html --public-url ${CONSOLE_PUBLIC_URL:-/console} --no-cache --hmr-port 6002",
|
||||||
"check": "tsc --noEmit",
|
"check": "tsc --noEmit",
|
||||||
"build": "pnpm check && rm -rf dist && parcel build src/index.html --no-autoinstall --no-cache --public-url /console",
|
"build": "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": "eslint --ext .ts --ext .tsx src",
|
||||||
"lint:report": "pnpm lint --format json --output-file report.json",
|
"lint:report": "pnpm lint --format json --output-file report.json",
|
||||||
"stylelint": "stylelint \"src/**/*.scss\""
|
"stylelint": "stylelint \"src/**/*.scss\""
|
||||||
|
|
|
@ -46,6 +46,7 @@ import {
|
||||||
SignInExperiencePage,
|
SignInExperiencePage,
|
||||||
UserDetailsTabs,
|
UserDetailsTabs,
|
||||||
adminTenantEndpoint,
|
adminTenantEndpoint,
|
||||||
|
getUserTenantId,
|
||||||
} from './consts';
|
} from './consts';
|
||||||
import AppContent from './containers/AppContent';
|
import AppContent from './containers/AppContent';
|
||||||
import AppEndpointsProvider, { AppEndpointsContext } from './containers/AppEndpointsProvider';
|
import AppEndpointsProvider, { AppEndpointsContext } from './containers/AppEndpointsProvider';
|
||||||
|
@ -63,7 +64,6 @@ import RoleUsers from './pages/RoleDetails/RoleUsers';
|
||||||
import UserLogs from './pages/UserDetails/UserLogs';
|
import UserLogs from './pages/UserDetails/UserLogs';
|
||||||
import UserRoles from './pages/UserDetails/UserRoles';
|
import UserRoles from './pages/UserDetails/UserRoles';
|
||||||
import UserSettings from './pages/UserDetails/UserSettings';
|
import UserSettings from './pages/UserDetails/UserSettings';
|
||||||
import { getBasename } from './utils/router';
|
|
||||||
|
|
||||||
void initI18n();
|
void initI18n();
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ const Main = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<BrowserRouter basename={getBasename('console', '5002')}>
|
<BrowserRouter basename={`/${getUserTenantId() ?? ''}`}>
|
||||||
<AppEndpointsProvider>
|
<AppEndpointsProvider>
|
||||||
<LogtoProvider
|
<LogtoProvider
|
||||||
config={{
|
config={{
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { defaultTenantId } from '@logto/schemas';
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
export const adminTenantEndpoint =
|
export const adminTenantEndpoint =
|
||||||
process.env.ADMIN_TENANT_ENDPOINT ??
|
process.env.ADMIN_TENANT_ENDPOINT ??
|
||||||
(isProduction ? window.location.origin : 'http://localhost:3002');
|
(isProduction ? window.location.origin : 'http://localhost:3002');
|
||||||
export const userTenantId = process.env.USER_TENANT_ID ?? defaultTenantId;
|
export const getUserTenantId = () => window.location.pathname.split('/')[1];
|
||||||
|
|
|
@ -2,7 +2,7 @@ import ky from 'ky';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useMemo, useEffect, createContext, useState } from 'react';
|
import { useMemo, useEffect, createContext, useState } from 'react';
|
||||||
|
|
||||||
import { adminTenantEndpoint, userTenantId } from '@/consts';
|
import { adminTenantEndpoint, getUserTenantId } from '@/consts';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -23,8 +23,14 @@ const AppEndpointsProvider = ({ children }: Props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getEndpoint = async () => {
|
const getEndpoint = async () => {
|
||||||
|
const tenantId = getUserTenantId();
|
||||||
|
|
||||||
|
if (!tenantId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { user } = await ky
|
const { user } = await ky
|
||||||
.get(new URL(`api/.well-known/endpoints/${userTenantId}`, adminTenantEndpoint))
|
.get(new URL(`api/.well-known/endpoints/${tenantId}`, adminTenantEndpoint))
|
||||||
.json<{ user: string }>();
|
.json<{ user: string }>();
|
||||||
setEndpoints({ userEndpoint: new URL(user) });
|
setEndpoints({ userEndpoint: new URL(user) });
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const getBasename = (prefix: string, developmentPort: string): string => {
|
|
||||||
const isBasenameNeeded =
|
|
||||||
process.env.NODE_ENV !== 'development' || process.env.PORT === developmentPort;
|
|
||||||
|
|
||||||
return isBasenameNeeded ? '/' + prefix : '';
|
|
||||||
};
|
|
|
@ -106,6 +106,7 @@ importers:
|
||||||
|
|
||||||
packages/cloud:
|
packages/cloud:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/schemas': workspace:*
|
||||||
'@logto/shared': workspace:*
|
'@logto/shared': workspace:*
|
||||||
'@silverhand/eslint-config': 2.0.1
|
'@silverhand/eslint-config': 2.0.1
|
||||||
'@silverhand/essentials': 2.2.0
|
'@silverhand/essentials': 2.2.0
|
||||||
|
@ -116,7 +117,9 @@ importers:
|
||||||
'@withtyped/postgres': ^0.6.0
|
'@withtyped/postgres': ^0.6.0
|
||||||
'@withtyped/server': ^0.6.0
|
'@withtyped/server': ^0.6.0
|
||||||
chalk: ^5.0.0
|
chalk: ^5.0.0
|
||||||
|
dotenv: ^16.0.0
|
||||||
eslint: ^8.21.0
|
eslint: ^8.21.0
|
||||||
|
find-up: ^6.3.0
|
||||||
http-proxy: ^1.18.1
|
http-proxy: ^1.18.1
|
||||||
lint-staged: ^13.0.0
|
lint-staged: ^13.0.0
|
||||||
mime-types: ^2.1.35
|
mime-types: ^2.1.35
|
||||||
|
@ -124,11 +127,14 @@ importers:
|
||||||
prettier: ^2.8.1
|
prettier: ^2.8.1
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@logto/schemas': link:../schemas
|
||||||
'@logto/shared': link:../shared
|
'@logto/shared': link:../shared
|
||||||
'@silverhand/essentials': 2.2.0
|
'@silverhand/essentials': 2.2.0
|
||||||
'@withtyped/postgres': 0.6.0_@withtyped+server@0.6.0
|
'@withtyped/postgres': 0.6.0_@withtyped+server@0.6.0
|
||||||
'@withtyped/server': 0.6.0
|
'@withtyped/server': 0.6.0
|
||||||
chalk: 5.1.2
|
chalk: 5.1.2
|
||||||
|
dotenv: 16.0.0
|
||||||
|
find-up: 6.3.0
|
||||||
http-proxy: 1.18.1
|
http-proxy: 1.18.1
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|
Loading…
Reference in a new issue