mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: fix alteration
This commit is contained in:
parent
99837b4e48
commit
6b09da2f5d
27 changed files with 161 additions and 108 deletions
10
.github/workflows/integration-test.yml
vendored
10
.github/workflows/integration-test.yml
vendored
|
@ -88,7 +88,7 @@ jobs:
|
||||||
|
|
||||||
- name: Run Logto
|
- name: Run Logto
|
||||||
working-directory: logto/
|
working-directory: logto/
|
||||||
run: npm start &
|
run: nohup npm start > nohup.out 2> nohup.err < /dev/null &
|
||||||
env:
|
env:
|
||||||
INTEGRATION_TEST: true
|
INTEGRATION_TEST: true
|
||||||
|
|
||||||
|
@ -101,3 +101,11 @@ jobs:
|
||||||
cd tests/packages/integration-tests
|
cd tests/packages/integration-tests
|
||||||
pnpm build
|
pnpm build
|
||||||
pnpm test:${{ matrix.test_target }}
|
pnpm test:${{ matrix.test_target }}
|
||||||
|
|
||||||
|
- name: Show logs
|
||||||
|
working-directory: logto/
|
||||||
|
run: cat nohup.out
|
||||||
|
|
||||||
|
- name: Show error logs
|
||||||
|
working-directory: logto/
|
||||||
|
run: cat nohup.err
|
||||||
|
|
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
@ -173,10 +173,6 @@ jobs:
|
||||||
run: node .scripts/compare-database.js fresh old
|
run: node .scripts/compare-database.js fresh old
|
||||||
# ** End **
|
# ** End **
|
||||||
|
|
||||||
- name: Check database
|
|
||||||
working-directory: ./fresh
|
|
||||||
run: node .scripts/check-database.js fresh
|
|
||||||
|
|
||||||
- name: Check alteration databases
|
- name: Check alteration databases
|
||||||
working-directory: ./fresh
|
working-directory: ./fresh
|
||||||
run: node .scripts/check-alterations-sequence.js
|
run: node .scripts/check-alterations-sequence.js
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
throw new Error('not implemented');
|
|
||||||
|
|
||||||
// TODO: check tables have tenant_id
|
|
|
@ -117,20 +117,12 @@ assert.deepStrictEqual(...manifests);
|
||||||
const queryDatabaseData = async (database) => {
|
const queryDatabaseData = async (database) => {
|
||||||
const pool = new pg.Pool({ database, user: 'postgres', password: 'postgres' });
|
const pool = new pg.Pool({ database, user: 'postgres', password: 'postgres' });
|
||||||
const result = await Promise.all(manifests[0].tables
|
const result = await Promise.all(manifests[0].tables
|
||||||
.filter(({ table_name }) => !['logto_configs', '_logto_configs'].includes(table_name))
|
// system configs are usually generated or time-relative, ignore for now
|
||||||
|
.filter(({ table_name }) => !['logto_configs', '_logto_configs', 'systems'].includes(table_name))
|
||||||
.map(async ({ table_name }) => {
|
.map(async ({ table_name }) => {
|
||||||
const { rows } = await pool.query(/* sql */`select * from ${table_name};`);
|
const { rows } = await pool.query(/* sql */`select * from ${table_name};`);
|
||||||
|
|
||||||
if (table_name === 'systems') {
|
return [table_name, omitArray(rows, 'created_at', 'updated_at', 'secret', 'db_user', 'db_user_password')];
|
||||||
return [
|
|
||||||
table_name,
|
|
||||||
rows.map(({ value, ...rest }) =>
|
|
||||||
({ ...rest, value: omit(value, 'createdAt', 'updatedAt') })
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [table_name, omitArray(rows, 'created_at', 'updated_at', 'secret')];
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createMockUtils } from '@logto/shared/esm';
|
import { createMockUtils } from '@logto/shared/esm';
|
||||||
import { createMockQueryResult, createMockPool } from 'slonik';
|
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
const { mockEsm, mockEsmWithActual, mockEsmDefault } = createMockUtils(jest);
|
const { mockEsm, mockEsmWithActual, mockEsmDefault } = createMockUtils(jest);
|
||||||
|
@ -12,6 +11,7 @@ process.env.DB_URL = 'postgres://mock.db.url';
|
||||||
process.env.ENDPOINT = 'https://logto.test';
|
process.env.ENDPOINT = 'https://logto.test';
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
/* Mock for EnvSet */
|
||||||
mockEsm('#src/libraries/logto-config.js', () => ({
|
mockEsm('#src/libraries/logto-config.js', () => ({
|
||||||
createLogtoConfigLibrary: () => ({ getOidcConfigs: () => ({}) }),
|
createLogtoConfigLibrary: () => ({ getOidcConfigs: () => ({}) }),
|
||||||
}));
|
}));
|
||||||
|
@ -24,6 +24,7 @@ mockEsm('#src/env-set/check-alteration-state.js', () => ({
|
||||||
mockEsmDefault('#src/env-set/oidc.js', () => () => ({
|
mockEsmDefault('#src/env-set/oidc.js', () => () => ({
|
||||||
issuer: 'https://logto.test/oidc',
|
issuer: 'https://logto.test/oidc',
|
||||||
}));
|
}));
|
||||||
|
/* End */
|
||||||
|
|
||||||
await mockEsmWithActual('#src/env-set/index.js', () => ({
|
await mockEsmWithActual('#src/env-set/index.js', () => ({
|
||||||
MountedApps: {
|
MountedApps: {
|
||||||
|
@ -33,18 +34,6 @@ await mockEsmWithActual('#src/env-set/index.js', () => ({
|
||||||
DemoApp: 'demo-app',
|
DemoApp: 'demo-app',
|
||||||
Welcome: 'welcome',
|
Welcome: 'welcome',
|
||||||
},
|
},
|
||||||
// TODO: Remove after clean up of default env sets
|
|
||||||
default: {
|
|
||||||
get oidc() {
|
|
||||||
return {
|
|
||||||
issuer: 'https://logto.test/oidc',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
get pool() {
|
|
||||||
return createMockPool({ query: async () => createMockQueryResult([]) });
|
|
||||||
},
|
|
||||||
load: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Logger is not considered in all test cases
|
// Logger is not considered in all test cases
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
"@logto/schemas": "workspace:*",
|
"@logto/schemas": "workspace:*",
|
||||||
"@logto/shared": "workspace:*",
|
"@logto/shared": "workspace:*",
|
||||||
"@silverhand/essentials": "2.1.0",
|
"@silverhand/essentials": "2.1.0",
|
||||||
"@withtyped/postgres": "^0.4.1",
|
"@withtyped/postgres": "^0.5.1",
|
||||||
"@withtyped/server": "^0.4.1",
|
"@withtyped/server": "^0.5.1",
|
||||||
"chalk": "^5.0.0",
|
"chalk": "^5.0.0",
|
||||||
"clean-deep": "^3.4.0",
|
"clean-deep": "^3.4.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
|
|
@ -17,8 +17,15 @@ const logListening = (type: 'core' | 'admin' = 'core') => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTenantId = () => {
|
const getTenantId = () => {
|
||||||
if (!EnvSet.values.isDomainBasedMultiTenancy) {
|
const { isDomainBasedMultiTenancy, isProduction, isIntegrationTest, developmentTenantId } =
|
||||||
return (!EnvSet.values.isProduction && EnvSet.values.developmentTenantId) || defaultTenant;
|
EnvSet.values;
|
||||||
|
|
||||||
|
if (!isDomainBasedMultiTenancy) {
|
||||||
|
if ((!isProduction || isIntegrationTest) && developmentTenantId) {
|
||||||
|
return developmentTenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { createLogtoConfigLibrary } from '#src/libraries/logto-config.js';
|
||||||
import { appendPath } from '#src/utils/url.js';
|
import { appendPath } from '#src/utils/url.js';
|
||||||
|
|
||||||
import GlobalValues from './GlobalValues.js';
|
import GlobalValues from './GlobalValues.js';
|
||||||
import { checkAlterationState } from './check-alteration-state.js';
|
|
||||||
import createPool from './create-pool.js';
|
import createPool from './create-pool.js';
|
||||||
import createQueryClient from './create-query-client.js';
|
import createQueryClient from './create-query-client.js';
|
||||||
import loadOidcValues from './oidc.js';
|
import loadOidcValues from './oidc.js';
|
||||||
|
@ -23,12 +22,20 @@ export enum MountedApps {
|
||||||
|
|
||||||
export class EnvSet {
|
export class EnvSet {
|
||||||
static values = new GlobalValues();
|
static values = new GlobalValues();
|
||||||
static default = new EnvSet(EnvSet.values.dbUrl);
|
|
||||||
|
|
||||||
static get isTest() {
|
static get isTest() {
|
||||||
return this.values.isTest;
|
return this.values.isTest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get dbUrl() {
|
||||||
|
return this.values.dbUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static queryClient = createQueryClient(this.dbUrl, this.isTest);
|
||||||
|
|
||||||
|
/** @deprecated Only for backward compatibility; Will be replaced soon. */
|
||||||
|
static pool = createPool(this.dbUrl, this.isTest);
|
||||||
|
|
||||||
#pool: Optional<DatabasePool>;
|
#pool: Optional<DatabasePool>;
|
||||||
// Use another pool for `withtyped` while adopting the new model,
|
// Use another pool for `withtyped` while adopting the new model,
|
||||||
// as we cannot extract the original PgPool from slonik
|
// as we cannot extract the original PgPool from slonik
|
||||||
|
@ -76,12 +83,11 @@ export class EnvSet {
|
||||||
this.#queryClient = createQueryClient(this.databaseUrl, EnvSet.isTest);
|
this.#queryClient = createQueryClient(this.databaseUrl, EnvSet.isTest);
|
||||||
|
|
||||||
const { getOidcConfigs } = createLogtoConfigLibrary(pool);
|
const { getOidcConfigs } = createLogtoConfigLibrary(pool);
|
||||||
const [, oidcConfigs] = await Promise.all([checkAlterationState(pool), getOidcConfigs()]);
|
|
||||||
|
const oidcConfigs = await getOidcConfigs();
|
||||||
this.#oidc = await loadOidcValues(
|
this.#oidc = await loadOidcValues(
|
||||||
appendPath(EnvSet.values.endpoint, '/oidc').toString(),
|
appendPath(EnvSet.values.endpoint, '/oidc').toString(),
|
||||||
oidcConfigs
|
oidcConfigs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await EnvSet.default.load();
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import dotenv from 'dotenv';
|
||||||
import { findUp } from 'find-up';
|
import { findUp } from 'find-up';
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
|
|
||||||
|
import { checkAlterationState } from './env-set/check-alteration-state.js';
|
||||||
|
|
||||||
dotenv.config({ path: await findUp('.env', {}) });
|
dotenv.config({ path: await findUp('.env', {}) });
|
||||||
|
|
||||||
// Import after env has been configured
|
// Import after env has been configured
|
||||||
|
@ -17,7 +19,10 @@ try {
|
||||||
});
|
});
|
||||||
await initI18n();
|
await initI18n();
|
||||||
await loadConnectorFactories();
|
await loadConnectorFactories();
|
||||||
await checkRowLevelSecurity(EnvSet.default.queryClient);
|
await Promise.all([
|
||||||
|
checkRowLevelSecurity(EnvSet.queryClient),
|
||||||
|
checkAlterationState(await EnvSet.pool),
|
||||||
|
]);
|
||||||
|
|
||||||
// Import last until init completed
|
// Import last until init completed
|
||||||
const { default: initApp } = await import('./app/init.js');
|
const { default: initApp } = await import('./app/init.js');
|
||||||
|
|
|
@ -27,7 +27,6 @@ const queryFunction = jest.fn();
|
||||||
|
|
||||||
const url = 'https://logto.gg';
|
const url = 'https://logto.gg';
|
||||||
const hook: InferModelType<ModelRouters['hook']['model']> = {
|
const hook: InferModelType<ModelRouters['hook']['model']> = {
|
||||||
tenantId: undefined,
|
|
||||||
id: 'foo',
|
id: 'foo',
|
||||||
event: HookEvent.PostSignIn,
|
event: HookEvent.PostSignIn,
|
||||||
config: { headers: { bar: 'baz' }, url, retries: 3 },
|
config: { headers: { bar: 'baz' }, url, retries: 3 },
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Sinon from 'sinon';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
import { mockEnvSet } from '#src/test-utils/env-set.js';
|
||||||
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
|
||||||
|
|
||||||
import type { WithAuthContext } from './koa-auth.js';
|
import type { WithAuthContext } from './koa-auth.js';
|
||||||
|
@ -63,7 +64,7 @@ describe('koaAuth middleware', () => {
|
||||||
developmentUserId: 'foo',
|
developmentUserId: 'foo',
|
||||||
});
|
});
|
||||||
|
|
||||||
await koaAuth(EnvSet.default)(ctx, next);
|
await koaAuth(mockEnvSet)(ctx, next);
|
||||||
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||||
|
|
||||||
stub.restore();
|
stub.restore();
|
||||||
|
@ -78,7 +79,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await koaAuth(EnvSet.default)(mockCtx, next);
|
await koaAuth(mockEnvSet)(mockCtx, next);
|
||||||
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ describe('koaAuth middleware', () => {
|
||||||
isIntegrationTest: true,
|
isIntegrationTest: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await koaAuth(EnvSet.default)(ctx, next);
|
await koaAuth(mockEnvSet)(ctx, next);
|
||||||
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
expect(ctx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||||
|
|
||||||
stub.restore();
|
stub.restore();
|
||||||
|
@ -111,7 +112,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await koaAuth(EnvSet.default)(mockCtx, next);
|
await koaAuth(mockEnvSet)(mockCtx, next);
|
||||||
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
expect(mockCtx.auth).toEqual({ type: 'user', id: 'foo' });
|
||||||
|
|
||||||
stub.restore();
|
stub.restore();
|
||||||
|
@ -124,12 +125,12 @@ describe('koaAuth middleware', () => {
|
||||||
authorization: 'Bearer access_token',
|
authorization: 'Bearer access_token',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await koaAuth(EnvSet.default)(ctx, next);
|
await koaAuth(mockEnvSet)(ctx, next);
|
||||||
expect(ctx.auth).toEqual({ type: 'user', id: 'fooUser' });
|
expect(ctx.auth).toEqual({ type: 'user', id: 'fooUser' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expect to throw if authorization header is missing', async () => {
|
it('expect to throw if authorization header is missing', async () => {
|
||||||
await expect(koaAuth(EnvSet.default)(ctx, next)).rejects.toMatchError(authHeaderMissingError);
|
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(authHeaderMissingError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expect to throw if authorization header token type not recognized ', async () => {
|
it('expect to throw if authorization header token type not recognized ', async () => {
|
||||||
|
@ -140,7 +141,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(koaAuth(EnvSet.default)(ctx, next)).rejects.toMatchError(tokenNotSupportedError);
|
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(tokenNotSupportedError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expect to throw if jwt sub is missing', async () => {
|
it('expect to throw if jwt sub is missing', async () => {
|
||||||
|
@ -153,7 +154,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(koaAuth(EnvSet.default)(ctx, next)).rejects.toMatchError(jwtSubMissingError);
|
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(jwtSubMissingError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expect to have `client` type per jwt verify result', async () => {
|
it('expect to have `client` type per jwt verify result', async () => {
|
||||||
|
@ -166,7 +167,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await koaAuth(EnvSet.default)(ctx, next);
|
await koaAuth(mockEnvSet)(ctx, next);
|
||||||
expect(ctx.auth).toEqual({ type: 'app', id: 'bar' });
|
expect(ctx.auth).toEqual({ type: 'app', id: 'bar' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(koaAuth(EnvSet.default, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
await expect(koaAuth(mockEnvSet, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||||
forbiddenError
|
forbiddenError
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -197,7 +198,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(koaAuth(EnvSet.default, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
await expect(koaAuth(mockEnvSet, UserRole.Admin)(ctx, next)).rejects.toMatchError(
|
||||||
forbiddenError
|
forbiddenError
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -213,7 +214,7 @@ describe('koaAuth middleware', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(koaAuth(EnvSet.default)(ctx, next)).rejects.toMatchError(
|
await expect(koaAuth(mockEnvSet)(ctx, next)).rejects.toMatchError(
|
||||||
new RequestError({ code: 'auth.unauthorized', status: 401 }, new Error('unknown error'))
|
new RequestError({ code: 'auth.unauthorized', status: 401 }, new Error('unknown error'))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createMockUtils } from '@logto/shared/esm';
|
||||||
import snakecaseKeys from 'snakecase-keys';
|
import snakecaseKeys from 'snakecase-keys';
|
||||||
|
|
||||||
import { mockApplication } from '#src/__mocks__/index.js';
|
import { mockApplication } from '#src/__mocks__/index.js';
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { mockEnvSet } from '#src/test-utils/env-set.js';
|
||||||
import { MockQueries } from '#src/test-utils/tenant.js';
|
import { MockQueries } from '#src/test-utils/tenant.js';
|
||||||
|
|
||||||
import { getConstantClientMetadata } from './utils.js';
|
import { getConstantClientMetadata } from './utils.js';
|
||||||
|
@ -48,7 +48,7 @@ const now = Date.now();
|
||||||
describe('postgres Adapter', () => {
|
describe('postgres Adapter', () => {
|
||||||
it('Client Modal', async () => {
|
it('Client Modal', async () => {
|
||||||
const rejectError = new Error('Not implemented');
|
const rejectError = new Error('Not implemented');
|
||||||
const adapter = postgresAdapter(EnvSet.default, queries, 'Client');
|
const adapter = postgresAdapter(mockEnvSet, queries, 'Client');
|
||||||
|
|
||||||
await expect(adapter.upsert('client', {}, 0)).rejects.toMatchError(rejectError);
|
await expect(adapter.upsert('client', {}, 0)).rejects.toMatchError(rejectError);
|
||||||
await expect(adapter.findByUserCode('foo')).rejects.toMatchError(rejectError);
|
await expect(adapter.findByUserCode('foo')).rejects.toMatchError(rejectError);
|
||||||
|
@ -72,7 +72,7 @@ describe('postgres Adapter', () => {
|
||||||
client_id,
|
client_id,
|
||||||
client_name,
|
client_name,
|
||||||
client_secret,
|
client_secret,
|
||||||
...getConstantClientMetadata(EnvSet.default, type),
|
...getConstantClientMetadata(mockEnvSet, type),
|
||||||
...snakecaseKeys(oidcClientMetadata),
|
...snakecaseKeys(oidcClientMetadata),
|
||||||
...customClientMetadata,
|
...customClientMetadata,
|
||||||
});
|
});
|
||||||
|
@ -85,7 +85,7 @@ describe('postgres Adapter', () => {
|
||||||
const id = 'fooId';
|
const id = 'fooId';
|
||||||
const grantId = 'grantId';
|
const grantId = 'grantId';
|
||||||
const expireAt = 60;
|
const expireAt = 60;
|
||||||
const adapter = postgresAdapter(EnvSet.default, queries, modelName);
|
const adapter = postgresAdapter(mockEnvSet, queries, modelName);
|
||||||
|
|
||||||
await adapter.upsert(id, { uid, userCode }, expireAt);
|
await adapter.upsert(id, { uid, userCode }, expireAt);
|
||||||
expect(upsertInstance).toBeCalledWith({
|
expect(upsertInstance).toBeCalledWith({
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { mockEnvSet } from '#src/test-utils/env-set.js';
|
||||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||||
|
|
||||||
import initOidc from './init.js';
|
import initOidc from './init.js';
|
||||||
|
@ -7,6 +7,6 @@ describe('oidc provider init', () => {
|
||||||
it('init should not throw', async () => {
|
it('init should not throw', async () => {
|
||||||
const { queries, libraries } = new MockTenant();
|
const { queries, libraries } = new MockTenant();
|
||||||
|
|
||||||
expect(() => initOidc(EnvSet.default, queries, libraries)).not.toThrow();
|
expect(() => initOidc(mockEnvSet, queries, libraries)).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApplicationType, CustomClientMetadataKey, GrantType } from '@logto/schemas';
|
import { ApplicationType, CustomClientMetadataKey, GrantType } from '@logto/schemas';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
import { mockEnvSet } from '#src/test-utils/env-set.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isOriginAllowed,
|
isOriginAllowed,
|
||||||
|
@ -10,22 +10,22 @@ import {
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
describe('getConstantClientMetadata()', () => {
|
describe('getConstantClientMetadata()', () => {
|
||||||
expect(getConstantClientMetadata(EnvSet.default, ApplicationType.SPA)).toEqual({
|
expect(getConstantClientMetadata(mockEnvSet, ApplicationType.SPA)).toEqual({
|
||||||
application_type: 'web',
|
application_type: 'web',
|
||||||
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
||||||
token_endpoint_auth_method: 'none',
|
token_endpoint_auth_method: 'none',
|
||||||
});
|
});
|
||||||
expect(getConstantClientMetadata(EnvSet.default, ApplicationType.Native)).toEqual({
|
expect(getConstantClientMetadata(mockEnvSet, ApplicationType.Native)).toEqual({
|
||||||
application_type: 'native',
|
application_type: 'native',
|
||||||
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
||||||
token_endpoint_auth_method: 'none',
|
token_endpoint_auth_method: 'none',
|
||||||
});
|
});
|
||||||
expect(getConstantClientMetadata(EnvSet.default, ApplicationType.Traditional)).toEqual({
|
expect(getConstantClientMetadata(mockEnvSet, ApplicationType.Traditional)).toEqual({
|
||||||
application_type: 'web',
|
application_type: 'web',
|
||||||
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
|
||||||
token_endpoint_auth_method: 'client_secret_basic',
|
token_endpoint_auth_method: 'client_secret_basic',
|
||||||
});
|
});
|
||||||
expect(getConstantClientMetadata(EnvSet.default, ApplicationType.MachineToMachine)).toEqual({
|
expect(getConstantClientMetadata(mockEnvSet, ApplicationType.MachineToMachine)).toEqual({
|
||||||
application_type: 'web',
|
application_type: 'web',
|
||||||
grant_types: [GrantType.ClientCredentials],
|
grant_types: [GrantType.ClientCredentials],
|
||||||
token_endpoint_auth_method: 'client_secret_basic',
|
token_endpoint_auth_method: 'client_secret_basic',
|
||||||
|
|
|
@ -25,6 +25,10 @@ const middlewareList = [
|
||||||
return mock;
|
return mock;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mockEsm('./utils.js', () => ({
|
||||||
|
getTenantDatabaseDsn: async () => 'postgres://mock.db.url',
|
||||||
|
}));
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
mockEsmDefault('#src/oidc/init.js', () => () => createMockProvider());
|
mockEsmDefault('#src/oidc/init.js', () => () => createMockProvider());
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { getTenantDatabaseDsn } from './utils.js';
|
||||||
export default class Tenant implements TenantContext {
|
export default class Tenant implements TenantContext {
|
||||||
static async create(id: string): Promise<Tenant> {
|
static async create(id: string): Promise<Tenant> {
|
||||||
// Treat the default database URL as the management URL
|
// Treat the default database URL as the management URL
|
||||||
const envSet = new EnvSet(await getTenantDatabaseDsn(EnvSet.default, id));
|
const envSet = new EnvSet(await getTenantDatabaseDsn(id));
|
||||||
await envSet.load();
|
await envSet.load();
|
||||||
|
|
||||||
return new Tenant(envSet, id);
|
return new Tenant(envSet, id);
|
||||||
|
|
|
@ -12,6 +12,7 @@ class TenantPool {
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Init tenant:', tenantId);
|
||||||
const newTenant = await Tenant.create(tenantId);
|
const newTenant = await Tenant.create(tenantId);
|
||||||
this.cache.set(tenantId, newTenant);
|
this.cache.set(tenantId, newTenant);
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,21 @@ import { identifier, sql } from '@withtyped/postgres';
|
||||||
import type { QueryClient } from '@withtyped/server';
|
import type { QueryClient } from '@withtyped/server';
|
||||||
import { parseDsn, stringifyDsn } from 'slonik';
|
import { parseDsn, stringifyDsn } from 'slonik';
|
||||||
|
|
||||||
import type { EnvSet } from '#src/env-set/index.js';
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is to fetch the tenant password for the corresponding Postgres user.
|
* This function is to fetch the tenant password for the corresponding Postgres user.
|
||||||
*
|
*
|
||||||
* In multi-tenancy mode, Logto should ALWAYS use a restricted user with RLS enforced to ensure data isolation between tenants.
|
* In multi-tenancy mode, Logto should ALWAYS use a restricted user with RLS enforced to ensure data isolation between tenants.
|
||||||
*/
|
*/
|
||||||
export const getTenantDatabaseDsn = async (defaultEnvSet: EnvSet, tenantId: string) => {
|
export const getTenantDatabaseDsn = async (tenantId: string) => {
|
||||||
|
const { queryClient, dbUrl } = EnvSet;
|
||||||
const {
|
const {
|
||||||
tableName,
|
tableName,
|
||||||
rawKeys: { id, dbUser, dbUserPassword },
|
rawKeys: { id, dbUser, dbUserPassword },
|
||||||
} = Tenants;
|
} = Tenants;
|
||||||
|
|
||||||
const { rows } = await defaultEnvSet.queryClient.query(sql`
|
const { rows } = await queryClient.query(sql`
|
||||||
select ${identifier(dbUser)}, ${identifier(dbUserPassword)}
|
select ${identifier(dbUser)}, ${identifier(dbUserPassword)}
|
||||||
from ${identifier(tableName)}
|
from ${identifier(tableName)}
|
||||||
where ${identifier(id)} = ${tenantId}
|
where ${identifier(id)} = ${tenantId}
|
||||||
|
@ -29,14 +30,14 @@ export const getTenantDatabaseDsn = async (defaultEnvSet: EnvSet, tenantId: stri
|
||||||
throw new Error(`Cannot find valid tenant credentials for ID ${tenantId}`);
|
throw new Error(`Cannot find valid tenant credentials for ID ${tenantId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = parseDsn(defaultEnvSet.databaseUrl);
|
const options = parseDsn(dbUrl);
|
||||||
const username = rows[0][dbUser];
|
const username = rows[0][dbUser];
|
||||||
const password = rows[0][dbUserPassword];
|
const password = rows[0][dbUserPassword];
|
||||||
|
|
||||||
return stringifyDsn({
|
return stringifyDsn({
|
||||||
...options,
|
...options,
|
||||||
username: conditional(!username && String(username)),
|
username: conditional(typeof username === 'string' && username),
|
||||||
password: conditional(!password && String(password)),
|
password: conditional(typeof password === 'string' && password),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,9 +49,7 @@ export const checkRowLevelSecurity = async (client: QueryClient) => {
|
||||||
and rowsecurity=false
|
and rowsecurity=false
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (
|
if (rows.some(({ tablename }) => tablename !== Systems.table)) {
|
||||||
rows.some(({ tablename }) => tablename !== Systems.table && tablename !== Tenants.tableName)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Row-level security has to be enforced on EVERY business table when starting Logto.\n' +
|
'Row-level security has to be enforced on EVERY business table when starting Logto.\n' +
|
||||||
`Found following table(s) without RLS: ${rows
|
`Found following table(s) without RLS: ${rows
|
||||||
|
|
5
packages/core/src/test-utils/env-set.ts
Normal file
5
packages/core/src/test-utils/env-set.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { EnvSet } from '#src/env-set/index.js';
|
||||||
|
|
||||||
|
export const mockEnvSet = new EnvSet(EnvSet.values.dbUrl);
|
||||||
|
|
||||||
|
await mockEnvSet.load();
|
|
@ -1,11 +1,11 @@
|
||||||
import { createMockPool, createMockQueryResult } from 'slonik';
|
import { createMockPool, createMockQueryResult } from 'slonik';
|
||||||
|
|
||||||
import { EnvSet } from '#src/env-set/index.js';
|
|
||||||
import { createModelRouters } from '#src/model-routers/index.js';
|
import { createModelRouters } from '#src/model-routers/index.js';
|
||||||
import Libraries from '#src/tenants/Libraries.js';
|
import Libraries from '#src/tenants/Libraries.js';
|
||||||
import Queries from '#src/tenants/Queries.js';
|
import Queries from '#src/tenants/Queries.js';
|
||||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||||
|
|
||||||
|
import { mockEnvSet } from './env-set.js';
|
||||||
import type { GrantMock } from './oidc-provider.js';
|
import type { GrantMock } from './oidc-provider.js';
|
||||||
import { createMockProvider } from './oidc-provider.js';
|
import { createMockProvider } from './oidc-provider.js';
|
||||||
import { MockQueryClient } from './query-client.js';
|
import { MockQueryClient } from './query-client.js';
|
||||||
|
@ -45,7 +45,7 @@ export type DeepPartial<T> = T extends object
|
||||||
export type Partial2<T> = { [key in keyof T]?: Partial<T[key]> };
|
export type Partial2<T> = { [key in keyof T]?: Partial<T[key]> };
|
||||||
|
|
||||||
export class MockTenant implements TenantContext {
|
export class MockTenant implements TenantContext {
|
||||||
public envSet = EnvSet.default;
|
public envSet = mockEnvSet;
|
||||||
public queries: Queries;
|
public queries: Queries;
|
||||||
public libraries: Libraries;
|
public libraries: Libraries;
|
||||||
public modelRouters = createModelRouters(new MockQueryClient());
|
public modelRouters = createModelRouters(new MockQueryClient());
|
||||||
|
|
|
@ -53,6 +53,6 @@
|
||||||
},
|
},
|
||||||
"prettier": "@silverhand/eslint-config/.prettierrc",
|
"prettier": "@silverhand/eslint-config/.prettierrc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@withtyped/server": "^0.4.1"
|
"@withtyped/server": "^0.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,15 +44,34 @@ const alteration: AlterationScript = {
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
alter table hooks
|
alter table hooks
|
||||||
add column tenant_id varchar(21) not null default 'default'
|
add column tenant_id varchar(21) not null default 'default'
|
||||||
references tenants (id) on update cascade on delete cascade;
|
references tenants (id) on update cascade on delete cascade,
|
||||||
|
alter column id type varchar(21); -- OK to downsize since we use length 21 for ID generation in core
|
||||||
|
|
||||||
alter table hooks
|
alter table hooks
|
||||||
alter column tenant_id drop default;
|
alter column tenant_id drop default;
|
||||||
|
|
||||||
create index hooks__id on hooks (tenant_id, id);
|
create index hooks__id on hooks (tenant_id, id);
|
||||||
|
|
||||||
|
drop index hooks__event;
|
||||||
|
create index hooks__event on hooks (tenant_id, event);
|
||||||
|
|
||||||
|
create trigger set_tenant_id before insert on hooks
|
||||||
|
for each row execute procedure set_tenant_id();
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add db_user column to tenants table
|
||||||
|
await pool.query(sql`
|
||||||
|
alter table tenants
|
||||||
|
add column db_user varchar(128),
|
||||||
|
add constraint tenants__db_user
|
||||||
|
unique (db_user);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Create role and setup privileges
|
// Create role and setup privileges
|
||||||
const baseRole = `logto_tenant_${database}`;
|
const baseRole = `logto_tenant_${database}`;
|
||||||
const baseRoleId = getId(baseRole);
|
const baseRoleId = getId(baseRole);
|
||||||
|
|
||||||
|
// See `_after_all.sql` for comments
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
create role ${baseRoleId} noinherit;
|
create role ${baseRoleId} noinherit;
|
||||||
|
|
||||||
|
@ -65,19 +84,21 @@ const alteration: AlterationScript = {
|
||||||
on table tenants
|
on table tenants
|
||||||
from ${baseRoleId};
|
from ${baseRoleId};
|
||||||
|
|
||||||
|
grant select (id, db_user)
|
||||||
|
on table tenants
|
||||||
|
to ${baseRoleId};
|
||||||
|
|
||||||
|
alter table tenants enable row level security;
|
||||||
|
|
||||||
|
create policy tenants_tenant_id on tenants
|
||||||
|
to ${baseRoleId}
|
||||||
|
using (db_user = current_user);
|
||||||
|
|
||||||
revoke all privileges
|
revoke all privileges
|
||||||
on table systems
|
on table systems
|
||||||
from ${baseRoleId};
|
from ${baseRoleId};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Add db_user column to tenants table
|
|
||||||
await pool.query(sql`
|
|
||||||
alter table tenants
|
|
||||||
add column db_user varchar(128),
|
|
||||||
add constraint tenants__db_user
|
|
||||||
unique (db_user);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Enable RLS
|
// Enable RLS
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tables.map(async (tableName) =>
|
tables.map(async (tableName) =>
|
||||||
|
@ -130,6 +151,9 @@ const alteration: AlterationScript = {
|
||||||
in schema public
|
in schema public
|
||||||
from ${baseRoleId};
|
from ${baseRoleId};
|
||||||
|
|
||||||
|
drop policy tenants_tenant_id on tenants;
|
||||||
|
alter table tenants disable row level security;
|
||||||
|
|
||||||
drop role ${baseRoleId};
|
drop role ${baseRoleId};
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
@ -139,13 +163,17 @@ const alteration: AlterationScript = {
|
||||||
drop column db_user;
|
drop column db_user;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log('3');
|
|
||||||
// Revert hooks table from multi-tenancy
|
// Revert hooks table from multi-tenancy
|
||||||
await pool.query(sql`
|
await pool.query(sql`
|
||||||
drop index hooks__id;
|
drop index hooks__id;
|
||||||
|
|
||||||
alter table hooks
|
alter table hooks
|
||||||
drop column tenant_id;
|
drop column tenant_id,
|
||||||
|
alter column id type varchar(32);
|
||||||
|
|
||||||
|
create index hooks__event on hooks (event);
|
||||||
|
|
||||||
|
drop trigger set_tenant_id on hooks;
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
"@logto/language-kit": "workspace:*",
|
"@logto/language-kit": "workspace:*",
|
||||||
"@logto/phrases": "workspace:*",
|
"@logto/phrases": "workspace:*",
|
||||||
"@logto/phrases-ui": "workspace:*",
|
"@logto/phrases-ui": "workspace:*",
|
||||||
"@withtyped/server": "^0.4.1",
|
"@withtyped/server": "^0.5.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ export const Hooks = createModel(/* sql */ `
|
||||||
|
|
||||||
create index hooks__event on hooks (tenant_id, event);
|
create index hooks__event on hooks (tenant_id, event);
|
||||||
`)
|
`)
|
||||||
.extend('tenantId', z.string().optional())
|
|
||||||
.extend('id', { default: () => generateStandardId(), readonly: true })
|
.extend('id', { default: () => generateStandardId(), readonly: true })
|
||||||
.extend('event', z.nativeEnum(HookEvent)) // Tried to use `.refine()` to show the correct error path, but not working.
|
.extend('event', z.nativeEnum(HookEvent)) // Tried to use `.refine()` to show the correct error path, but not working.
|
||||||
.extend('config', hookConfigGuard);
|
.extend('config', hookConfigGuard)
|
||||||
|
.exclude('tenantId');
|
||||||
|
|
|
@ -10,6 +10,5 @@ export const Tenants = createModel(/* sql */ `
|
||||||
constraint tenants__db_user
|
constraint tenants__db_user
|
||||||
unique (db_user)
|
unique (db_user)
|
||||||
);
|
);
|
||||||
|
|
||||||
/* no_after_each */
|
/* no_after_each */
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -5,10 +5,27 @@ grant select, insert, update, delete
|
||||||
in schema public
|
in schema public
|
||||||
to logto_tenant_${database};
|
to logto_tenant_${database};
|
||||||
|
|
||||||
|
-- Security policies for tenants table --
|
||||||
|
|
||||||
revoke all privileges
|
revoke all privileges
|
||||||
on table tenants
|
on table tenants
|
||||||
from logto_tenant_${database};
|
from logto_tenant_${database};
|
||||||
|
|
||||||
|
/* Allow limited select to perform RLS query in `after_each` (using select ... from tenants ...) */
|
||||||
|
grant select (id, db_user)
|
||||||
|
on table tenants
|
||||||
|
to logto_tenant_${database};
|
||||||
|
|
||||||
|
alter table tenants enable row level security;
|
||||||
|
|
||||||
|
/* Create RLS policy to minimize the privilege */
|
||||||
|
create policy tenants_tenant_id on tenants
|
||||||
|
to logto_tenant_${database}
|
||||||
|
using (db_user = current_user);
|
||||||
|
|
||||||
|
-- End --
|
||||||
|
|
||||||
|
/* Revoke all privileges on systems table for tenant roles */
|
||||||
revoke all privileges
|
revoke all privileges
|
||||||
on table systems
|
on table systems
|
||||||
from logto_tenant_${database};
|
from logto_tenant_${database};
|
||||||
|
|
|
@ -276,8 +276,8 @@ importers:
|
||||||
'@types/semver': ^7.3.12
|
'@types/semver': ^7.3.12
|
||||||
'@types/sinon': ^10.0.13
|
'@types/sinon': ^10.0.13
|
||||||
'@types/supertest': ^2.0.11
|
'@types/supertest': ^2.0.11
|
||||||
'@withtyped/postgres': ^0.4.1
|
'@withtyped/postgres': ^0.5.1
|
||||||
'@withtyped/server': ^0.4.1
|
'@withtyped/server': ^0.5.1
|
||||||
chalk: ^5.0.0
|
chalk: ^5.0.0
|
||||||
clean-deep: ^3.4.0
|
clean-deep: ^3.4.0
|
||||||
copyfiles: ^2.4.1
|
copyfiles: ^2.4.1
|
||||||
|
@ -335,8 +335,8 @@ importers:
|
||||||
'@logto/schemas': link:../schemas
|
'@logto/schemas': link:../schemas
|
||||||
'@logto/shared': link:../shared
|
'@logto/shared': link:../shared
|
||||||
'@silverhand/essentials': 2.1.0
|
'@silverhand/essentials': 2.1.0
|
||||||
'@withtyped/postgres': 0.4.1_@withtyped+server@0.4.1
|
'@withtyped/postgres': 0.5.1_@withtyped+server@0.5.1
|
||||||
'@withtyped/server': 0.4.1
|
'@withtyped/server': 0.5.1
|
||||||
chalk: 5.1.2
|
chalk: 5.1.2
|
||||||
clean-deep: 3.4.0
|
clean-deep: 3.4.0
|
||||||
date-fns: 2.29.3
|
date-fns: 2.29.3
|
||||||
|
@ -481,7 +481,7 @@ importers:
|
||||||
'@types/jest': ^29.1.2
|
'@types/jest': ^29.1.2
|
||||||
'@types/jest-environment-puppeteer': ^5.0.2
|
'@types/jest-environment-puppeteer': ^5.0.2
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
'@withtyped/server': ^0.4.1
|
'@withtyped/server': ^0.5.1
|
||||||
dotenv: ^16.0.0
|
dotenv: ^16.0.0
|
||||||
eslint: ^8.21.0
|
eslint: ^8.21.0
|
||||||
got: ^12.5.3
|
got: ^12.5.3
|
||||||
|
@ -495,7 +495,7 @@ importers:
|
||||||
text-encoder: ^0.0.4
|
text-encoder: ^0.0.4
|
||||||
typescript: ^4.9.4
|
typescript: ^4.9.4
|
||||||
dependencies:
|
dependencies:
|
||||||
'@withtyped/server': 0.4.1
|
'@withtyped/server': 0.5.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@jest/types': 29.1.2
|
'@jest/types': 29.1.2
|
||||||
'@logto/connector-kit': link:../toolkit/connector-kit
|
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||||
|
@ -585,7 +585,7 @@ importers:
|
||||||
'@types/jest': ^29.1.2
|
'@types/jest': ^29.1.2
|
||||||
'@types/node': ^18.11.18
|
'@types/node': ^18.11.18
|
||||||
'@types/pluralize': ^0.0.29
|
'@types/pluralize': ^0.0.29
|
||||||
'@withtyped/server': ^0.4.1
|
'@withtyped/server': ^0.5.1
|
||||||
camelcase: ^7.0.0
|
camelcase: ^7.0.0
|
||||||
eslint: ^8.21.0
|
eslint: ^8.21.0
|
||||||
jest: ^29.1.2
|
jest: ^29.1.2
|
||||||
|
@ -603,7 +603,7 @@ importers:
|
||||||
'@logto/language-kit': link:../toolkit/language-kit
|
'@logto/language-kit': link:../toolkit/language-kit
|
||||||
'@logto/phrases': link:../phrases
|
'@logto/phrases': link:../phrases
|
||||||
'@logto/phrases-ui': link:../phrases-ui
|
'@logto/phrases-ui': link:../phrases-ui
|
||||||
'@withtyped/server': 0.4.1
|
'@withtyped/server': 0.5.1
|
||||||
zod: 3.20.2
|
zod: 3.20.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
|
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
|
||||||
|
@ -4463,21 +4463,21 @@ packages:
|
||||||
eslint-visitor-keys: 3.3.0
|
eslint-visitor-keys: 3.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@withtyped/postgres/0.4.1_@withtyped+server@0.4.1:
|
/@withtyped/postgres/0.5.1_@withtyped+server@0.5.1:
|
||||||
resolution: {integrity: sha512-UZtwUieJyj3tHxGgiskBPFefpkFZgv7yzlXFMmuyG2NNu6P8mFnCSp4vuycmGEJnYRLFrxDvLhjx1qpVlD3k6A==}
|
resolution: {integrity: sha512-Le4iIHEc4LRgDn4rjnwbGJ/J15PpqEoltgoZAOhYgnZznKBzkp4W3vxbav29x7IMOvzgum+Jo5HOW1q0kRfROg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@withtyped/server': ^0.4.1
|
'@withtyped/server': ^0.5.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/pg': 8.6.6
|
'@types/pg': 8.6.6
|
||||||
'@withtyped/server': 0.4.1
|
'@withtyped/server': 0.5.1
|
||||||
'@withtyped/shared': 0.2.0
|
'@withtyped/shared': 0.2.0
|
||||||
pg: 8.8.0
|
pg: 8.8.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- pg-native
|
- pg-native
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@withtyped/server/0.4.1:
|
/@withtyped/server/0.5.1:
|
||||||
resolution: {integrity: sha512-QMhFntmF2o/e9dEJi84+RL+BySzV6+uayY1dL372OmYCcYftPUYYCctvpTaz5eWUipqV/hL4FaEpK1YVEKYPHQ==}
|
resolution: {integrity: sha512-CR7Y4R2YsUNJ7STEzhJjBjCKIJg49r2Jun5tFuTmmH8IAdHacisWPuKyGMz8o8jnatGTBRJNvc2wjjhg0l8ptw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@withtyped/shared': 0.2.0
|
'@withtyped/shared': 0.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
Loading…
Reference in a new issue