diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ef0dfd3d..d69e1c720 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "typescript.tsdk": "node_modules/typescript/lib", "[scss]": { "editor.codeActionsOnSave": { - "source.fixAll.stylelint": true + "source.fixAll.stylelint": "explicit" } }, "stylelint.validate": [ @@ -24,7 +24,7 @@ "typescriptreact", ], "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, + "source.fixAll.eslint": "explicit" }, "json.schemas": [ { diff --git a/packages/core/src/database/find-entity-by-id.ts b/packages/core/src/database/find-entity-by-id.ts index 89a8720d0..95076d9fa 100644 --- a/packages/core/src/database/find-entity-by-id.ts +++ b/packages/core/src/database/find-entity-by-id.ts @@ -44,3 +44,26 @@ export const buildFindEntityByIdWithPool = } }; }; + +export const buildFindEntitiesByIdsWithPool = + (pool: CommonQueryMethods) => + < + Key extends string, + CreateSchema extends Partial>>, + Schema extends SchemaLike>, + >( + schema: GeneratedSchema, CreateSchema, Schema> + ) => { + const { table, fields } = convertToIdentifiers(schema); + const isKeyOfSchema = isKeyOf(schema); + + // Make sure id is key of the schema + assertThat(isKeyOfSchema('id'), 'entity.not_exists'); + + return async (ids: string[]) => + pool.any(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.id} in (${ids.length > 0 ? sql.join(ids, sql`, `) : sql`null`}) + `); + }; diff --git a/packages/core/src/libraries/application.ts b/packages/core/src/libraries/application.ts index 1ec4cc5b5..c6727d02e 100644 --- a/packages/core/src/libraries/application.ts +++ b/packages/core/src/libraries/application.ts @@ -1,12 +1,21 @@ import type { Scope } from '@logto/schemas'; +import RequestError from '#src/errors/RequestError/index.js'; import type Queries from '#src/tenants/Queries.js'; +import assertThat from '#src/utils/assert-that.js'; export const createApplicationLibrary = (queries: Queries) => { const { + applications: { + findApplicationById, + userConsentOrganizationScopes, + userConsentResourceScopes, + useConsentUserScopes, + }, applicationsRoles: { findApplicationsRolesByApplicationId }, rolesScopes: { findRolesScopesByRoleIds }, - scopes: { findScopesByIdsAndResourceIndicator }, + organizations: { scopes: organizationScopesQuery }, + scopes: { findScopesByIdsAndResourceIndicator, findScopesByIds }, } = queries; const findApplicationScopesForResourceIndicator = async ( @@ -25,7 +34,88 @@ export const createApplicationLibrary = (queries: Queries) => { return scopes; }; + // Guard application exists and is a third party application + const validateThirdPartyApplicationById = async (applicationId: string) => { + const application = await findApplicationById(applicationId); + + assertThat( + application.isThirdParty, + 'application.user_consent_scopes_only_for_third_party_applications' + ); + }; + + // Guard that all scopes exist + const validateApplicationUserConsentScopes = async ({ + organizationScopes = [], + resourceScopes = [], + }: { + organizationScopes?: string[]; + resourceScopes?: string[]; + }) => { + const [organizationScopesData, resourceScopesData] = await Promise.all([ + organizationScopesQuery.findByIds(organizationScopes), + findScopesByIds(resourceScopes), + ]); + + // Assert that all scopes exist, return the missing ones + const invalidOrganizationScopes = organizationScopes.filter( + (scope) => !organizationScopesData.some(({ id }) => id === scope) + ); + + const invalidResourceScopes = resourceScopes.filter( + (scope) => !resourceScopesData.some(({ id }) => id === scope) + ); + + assertThat( + invalidOrganizationScopes.length === 0 && invalidResourceScopes.length === 0, + new RequestError( + { + code: 'application.user_consent_scopes_not_found', + status: 422, + }, + { invalidOrganizationScopes, invalidResourceScopes } + ) + ); + }; + + // Assign consent scopes to application + const assignApplicationUserConsentScopes = async ( + applicationId: string, + { + organizationScopes, + resourceScopes, + userScopes, + }: { + organizationScopes?: string[]; + resourceScopes?: string[]; + userScopes?: string[]; + } + ) => { + if (organizationScopes) { + await userConsentOrganizationScopes.insert( + ...organizationScopes.map<[string, string]>((scope) => [applicationId, scope]) + ); + } + + if (resourceScopes) { + await userConsentResourceScopes.insert( + ...resourceScopes.map<[string, string]>((scope) => [applicationId, scope]) + ); + } + + if (userScopes) { + await Promise.all( + userScopes.map(async (userScope) => + useConsentUserScopes.insert({ applicationId, userScope }) + ) + ); + } + }; + return { + validateThirdPartyApplicationById, findApplicationScopesForResourceIndicator, + validateApplicationUserConsentScopes, + assignApplicationUserConsentScopes, }; }; diff --git a/packages/core/src/queries/application-user-consent-scopes.ts b/packages/core/src/queries/application-user-consent-scopes.ts new file mode 100644 index 000000000..995c01c6a --- /dev/null +++ b/packages/core/src/queries/application-user-consent-scopes.ts @@ -0,0 +1,40 @@ +import { + ApplicationUserConsentOrganizationScopes, + ApplicationUserConsentResourceScopes, + ApplicationUserConsentUserScopes, + Applications, + OrganizationScopes, + Scopes, +} from '@logto/schemas'; +import { type CommonQueryMethods } from 'slonik'; + +import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; +import { TwoRelationsQueries } from '#src/utils/RelationQueries.js'; + +export class ApplicationUserConsentOrganizationScopeQueries extends TwoRelationsQueries< + typeof Applications, + typeof OrganizationScopes +> { + constructor(pool: CommonQueryMethods) { + super(pool, ApplicationUserConsentOrganizationScopes.table, Applications, OrganizationScopes); + } +} + +export class ApplicationUserConsentResourceScopeQueries extends TwoRelationsQueries< + typeof Applications, + typeof Scopes +> { + constructor(pool: CommonQueryMethods) { + super(pool, ApplicationUserConsentResourceScopes.table, Applications, Scopes); + } +} + +export const createApplicationUserConsentUserScopeQueries = (pool: CommonQueryMethods) => { + const insert = buildInsertIntoWithPool(pool)(ApplicationUserConsentUserScopes, { + onConflict: { ignore: true }, + }); + + return { + insert, + }; +}; diff --git a/packages/core/src/queries/application.ts b/packages/core/src/queries/application.ts index 29e574118..52a92c5e3 100644 --- a/packages/core/src/queries/application.ts +++ b/packages/core/src/queries/application.ts @@ -13,6 +13,12 @@ import { DeletionError } from '#src/errors/SlonikError/index.js'; import { buildConditionsFromSearch } from '#src/utils/search.js'; import type { Search } from '#src/utils/search.js'; +import { + ApplicationUserConsentOrganizationScopeQueries, + ApplicationUserConsentResourceScopeQueries, + createApplicationUserConsentUserScopeQueries, +} from './application-user-consent-scopes.js'; + const { table, fields } = convertToIdentifiers(Applications); const buildApplicationConditions = (search: Search) => { @@ -233,5 +239,8 @@ export const createApplicationQueries = (pool: CommonQueryMethods) => { findM2mApplicationsByIds, findApplicationsByIds, deleteApplicationById, + userConsentOrganizationScopes: new ApplicationUserConsentOrganizationScopeQueries(pool), + userConsentResourceScopes: new ApplicationUserConsentResourceScopeQueries(pool), + useConsentUserScopes: createApplicationUserConsentUserScopeQueries(pool), }; }; diff --git a/packages/core/src/routes/applications/application-user-consent-scope.openapi.json b/packages/core/src/routes/applications/application-user-consent-scope.openapi.json new file mode 100644 index 000000000..729d63cb5 --- /dev/null +++ b/packages/core/src/routes/applications/application-user-consent-scope.openapi.json @@ -0,0 +1,40 @@ +{ + "paths": { + "/api/applications/{applicationId}/user-consent-scopes": { + "post": { + "summary": "Assign user consent scopes to application.", + "description": "Assign the user consent scopes to an application by application id", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "organizationScopes": { + "description": "A list of organization scope id to assign to the application. Throws error if any given organization scope is not found." + }, + "resourceScopes": { + "description": "A list of resource scope id to assign to the application. Throws error if any given resource scope is not found." + }, + "userScopes": { + "description": "A list of user scope enum value to assign to the application." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "All the user consent scopes are assigned to the application successfully" + }, + "404": { + "description": "The application is not found" + }, + "422": { + "description": "Any of the given organization scope, resource scope or user scope is not found" + } + } + } + } + } +} diff --git a/packages/core/src/routes/applications/application-user-consent-scope.ts b/packages/core/src/routes/applications/application-user-consent-scope.ts new file mode 100644 index 000000000..5beed2739 --- /dev/null +++ b/packages/core/src/routes/applications/application-user-consent-scope.ts @@ -0,0 +1,52 @@ +import { UserScope } from '@logto/core-kit'; +import { object, string, nativeEnum } from 'zod'; + +import koaGuard from '#src/middleware/koa-guard.js'; + +import type { AuthedRouter, RouterInitArgs } from '../types.js'; + +export default function applicationUserConsentScopeRoutes( + ...[ + router, + { + libraries: { + applications: { + validateThirdPartyApplicationById, + validateApplicationUserConsentScopes, + assignApplicationUserConsentScopes, + }, + }, + }, + ]: RouterInitArgs +) { + router.post( + '/applications/:applicationId/user-consent-scopes', + koaGuard({ + params: object({ + applicationId: string(), + }), + body: object({ + organizationScopes: string().array().optional(), + resourceScopes: string().array().optional(), + userScopes: nativeEnum(UserScope).array().optional(), + }), + status: [201, 404, 422], + }), + async (ctx, next) => { + const { + params: { applicationId }, + body, + } = ctx.guard; + + await validateThirdPartyApplicationById(applicationId); + + await validateApplicationUserConsentScopes(body); + + await assignApplicationUserConsentScopes(applicationId, body); + + ctx.status = 201; + + return next(); + } + ); +} diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index c0e7243fa..b2f64b3e5 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -12,6 +12,7 @@ import koaAuth from '../middleware/koa-auth/index.js'; import adminUserRoutes from './admin-user/index.js'; import applicationRoleRoutes from './applications/application-role.js'; +import applicationUserConsentScopeRoutes from './applications/application-user-consent-scope.js'; import applicationRoutes from './applications/application.js'; import authnRoutes from './authn.js'; import connectorRoutes from './connector/index.js'; @@ -45,6 +46,11 @@ const createRouters = (tenant: TenantContext) => { applicationRoutes(managementRouter, tenant); applicationRoleRoutes(managementRouter, tenant); + + if (EnvSet.values.isDevFeaturesEnabled) { + applicationUserConsentScopeRoutes(managementRouter, tenant); + } + logtoConfigRoutes(managementRouter, tenant); connectorRoutes(managementRouter, tenant); resourceRoutes(managementRouter, tenant); diff --git a/packages/core/src/utils/SchemaQueries.ts b/packages/core/src/utils/SchemaQueries.ts index f854b2f8e..f9c481778 100644 --- a/packages/core/src/utils/SchemaQueries.ts +++ b/packages/core/src/utils/SchemaQueries.ts @@ -4,7 +4,10 @@ import { type CommonQueryMethods } from 'slonik'; import { buildDeleteByIdWithPool } from '#src/database/delete-by-id.js'; import { buildFindAllEntitiesWithPool } from '#src/database/find-all-entities.js'; -import { buildFindEntityByIdWithPool } from '#src/database/find-entity-by-id.js'; +import { + buildFindEntitiesByIdsWithPool, + buildFindEntityByIdWithPool, +} from '#src/database/find-entity-by-id.js'; import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; import { buildGetTotalRowCountWithPool } from '#src/database/row-count.js'; import { buildUpdateWhereWithPool } from '#src/database/update-where.js'; @@ -31,6 +34,9 @@ export default class SchemaQueries< ) => Promise; #findById: (id: string) => Promise>; + + #findByIds: (ids: string[]) => Promise; + #insert: (data: OmitAutoSetFields) => Promise>; #updateById: ( @@ -47,6 +53,7 @@ export default class SchemaQueries< this.#findTotalNumber = buildGetTotalRowCountWithPool(this.pool, this.schema); this.#findAll = buildFindAllEntitiesWithPool(this.pool)(this.schema, orderBy && [orderBy]); this.#findById = buildFindEntityByIdWithPool(this.pool)(this.schema); + this.#findByIds = buildFindEntitiesByIdsWithPool(this.pool)(this.schema); this.#insert = buildInsertIntoWithPool(this.pool)(this.schema, { returning: true }); this.#updateById = buildUpdateWhereWithPool(this.pool)(this.schema, true); this.#deleteById = buildDeleteByIdWithPool(this.pool, this.schema.table); @@ -64,6 +71,10 @@ export default class SchemaQueries< return this.#findById(id); } + async findByIds(ids: string[]): Promise { + return this.#findByIds(ids); + } + async insert(data: CreateSchema): Promise> { return this.#insert(data); } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 99f559d77..4746acb80 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -25,6 +25,7 @@ "@jest/test-sequencer": "^29.5.0", "@jest/types": "^29.1.2", "@logto/connector-kit": "workspace:^2.0.0", + "@logto/core-kit": "workspace:^", "@logto/js": "^3.0.1", "@logto/node": "^2.2.0", "@logto/schemas": "workspace:^1.12.0", diff --git a/packages/integration-tests/src/api/application-user-consent-scope.ts b/packages/integration-tests/src/api/application-user-consent-scope.ts new file mode 100644 index 000000000..b153da186 --- /dev/null +++ b/packages/integration-tests/src/api/application-user-consent-scope.ts @@ -0,0 +1,12 @@ +import { type UserScope } from '@logto/core-kit'; + +import { authedAdminApi } from './api.js'; + +export const assignUserConsentScopes = async ( + applicationId: string, + payload: { + organizationScopes?: string[]; + resourceScopes?: string[]; + userScopes?: UserScope[]; + } +) => authedAdminApi.post(`applications/${applicationId}/user-consent-scopes`, { json: payload }); diff --git a/packages/integration-tests/src/tests/api/application-user-consent-scope.test.ts b/packages/integration-tests/src/tests/api/application-user-consent-scope.test.ts new file mode 100644 index 000000000..39a4524cc --- /dev/null +++ b/packages/integration-tests/src/tests/api/application-user-consent-scope.test.ts @@ -0,0 +1,125 @@ +import { UserScope } from '@logto/core-kit'; +import { ApplicationType } from '@logto/schemas'; + +import { assignUserConsentScopes } from '#src/api/application-user-consent-scope.js'; +import { createApplication, deleteApplication } from '#src/api/application.js'; +import { OrganizationScopeApi } from '#src/api/organization-scope.js'; +import { createResource, deleteResource } from '#src/api/resource.js'; +import { createScope } from '#src/api/scope.js'; +import { expectRejects } from '#src/helpers/index.js'; + +describe('assign user consent scopes to application', () => { + const applicationIds = new Map(); + const organizationScopes = new Map(); + const resourceScopes = new Map(); + const resourceIds = new Set(); + + const organizationScopeApi = new OrganizationScopeApi(); + + beforeAll(async () => { + const firstPartyApp = await createApplication('first-party-application', ApplicationType.SPA); + const thirdPartyApp = await createApplication( + 'third-party-application', + ApplicationType.Traditional, + { + isThirdParty: true, + } + ); + + applicationIds.set('firstPartyApp', firstPartyApp.id); + applicationIds.set('thirdPartyApp', thirdPartyApp.id); + + const organizationScope1 = await organizationScopeApi.create({ + name: 'organization-scope-1', + }); + + const organizationScope2 = await organizationScopeApi.create({ + name: 'organization-scope-2', + }); + + organizationScopes.set('organizationScope1', organizationScope1.id); + organizationScopes.set('organizationScope2', organizationScope2.id); + + const resource = await createResource(); + resourceIds.add(resource.id); + + const resourceScope1 = await createScope(resource.id); + const resourceScope2 = await createScope(resource.id); + + resourceScopes.set('resourceScope1', resourceScope1.id); + resourceScopes.set('resourceScope2', resourceScope2.id); + }); + + afterAll(async () => { + await Promise.all( + Array.from(resourceIds).map(async (resourceId) => deleteResource(resourceId)) + ); + await Promise.all( + Array.from(organizationScopes.values()).map(async (organizationScopeId) => + organizationScopeApi.delete(organizationScopeId) + ) + ); + await Promise.all( + Array.from(applicationIds.values()).map(async (applicationId) => + deleteApplication(applicationId) + ) + ); + }); + + it('should throw error when trying to assign scopes to non-third-party application', async () => { + await expectRejects( + assignUserConsentScopes(applicationIds.get('firstPartyApp')!, { + organizationScopes: Array.from(organizationScopes.values()), + resourceScopes: Array.from(resourceScopes.values()), + }), + { + code: 'application.user_consent_scopes_only_for_third_party_applications', + statusCode: 400, + } + ); + }); + + it('should throw error when trying to assign a non-existing organization scope', async () => { + await expectRejects( + assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { + organizationScopes: ['non-existing-organization-scope'], + }), + { + code: 'application.user_consent_scopes_not_found', + statusCode: 422, + } + ); + }); + + it('should throw error when trying to assign a non-existing resource scope', async () => { + await expectRejects( + assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { + resourceScopes: ['non-existing-resource-scope'], + }), + { + code: 'application.user_consent_scopes_not_found', + statusCode: 422, + } + ); + }); + + it('should assign scopes to third-party application successfully', async () => { + await expect( + assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { + organizationScopes: Array.from(organizationScopes.values()), + resourceScopes: Array.from(resourceScopes.values()), + userScopes: [UserScope.Profile, UserScope.Email, UserScope.OrganizationRoles], + }) + ).resolves.not.toThrow(); + }); + + it('should not throw error when trying to assign existing consent scopes', async () => { + await expect( + assignUserConsentScopes(applicationIds.get('thirdPartyApp')!, { + organizationScopes: [organizationScopes.get('organizationScope1')!], + resourceScopes: [resourceScopes.get('resourceScope1')!], + userScopes: [UserScope.Profile], + }) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/phrases/src/locales/de/errors/application.ts b/packages/phrases/src/locales/de/errors/application.ts index 8cd5852f7..da42d4386 100644 --- a/packages/phrases/src/locales/de/errors/application.ts +++ b/packages/phrases/src/locales/de/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/en/errors/application.ts b/packages/phrases/src/locales/en/errors/application.ts index 0ae69535f..5f394e445 100644 --- a/packages/phrases/src/locales/en/errors/application.ts +++ b/packages/phrases/src/locales/en/errors/application.ts @@ -4,6 +4,9 @@ const application = { invalid_role_type: 'Can not assign user type role to machine to machine application.', invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/es/errors/application.ts b/packages/phrases/src/locales/es/errors/application.ts index 12cd0d93d..95dac44fb 100644 --- a/packages/phrases/src/locales/es/errors/application.ts +++ b/packages/phrases/src/locales/es/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/fr/errors/application.ts b/packages/phrases/src/locales/fr/errors/application.ts index d5eda1dc4..08daa8b1d 100644 --- a/packages/phrases/src/locales/fr/errors/application.ts +++ b/packages/phrases/src/locales/fr/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/it/errors/application.ts b/packages/phrases/src/locales/it/errors/application.ts index e82fcd7ff..986d23530 100644 --- a/packages/phrases/src/locales/it/errors/application.ts +++ b/packages/phrases/src/locales/it/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/ja/errors/application.ts b/packages/phrases/src/locales/ja/errors/application.ts index 9403c5d3b..54531312a 100644 --- a/packages/phrases/src/locales/ja/errors/application.ts +++ b/packages/phrases/src/locales/ja/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/ko/errors/application.ts b/packages/phrases/src/locales/ko/errors/application.ts index 7dc80c1e8..93ac88c4a 100644 --- a/packages/phrases/src/locales/ko/errors/application.ts +++ b/packages/phrases/src/locales/ko/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/pl-pl/errors/application.ts b/packages/phrases/src/locales/pl-pl/errors/application.ts index 97bd43fdd..9dd584df0 100644 --- a/packages/phrases/src/locales/pl-pl/errors/application.ts +++ b/packages/phrases/src/locales/pl-pl/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/pt-br/errors/application.ts b/packages/phrases/src/locales/pt-br/errors/application.ts index 764cd951c..4747f5cbd 100644 --- a/packages/phrases/src/locales/pt-br/errors/application.ts +++ b/packages/phrases/src/locales/pt-br/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/pt-pt/errors/application.ts b/packages/phrases/src/locales/pt-pt/errors/application.ts index fea3624ab..b2daba762 100644 --- a/packages/phrases/src/locales/pt-pt/errors/application.ts +++ b/packages/phrases/src/locales/pt-pt/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/ru/errors/application.ts b/packages/phrases/src/locales/ru/errors/application.ts index 42639d970..7f4423e68 100644 --- a/packages/phrases/src/locales/ru/errors/application.ts +++ b/packages/phrases/src/locales/ru/errors/application.ts @@ -6,6 +6,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/tr-tr/errors/application.ts b/packages/phrases/src/locales/tr-tr/errors/application.ts index 4084dcdae..d44b7f163 100644 --- a/packages/phrases/src/locales/tr-tr/errors/application.ts +++ b/packages/phrases/src/locales/tr-tr/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/zh-cn/errors/application.ts b/packages/phrases/src/locales/zh-cn/errors/application.ts index b57469102..bf96650fb 100644 --- a/packages/phrases/src/locales/zh-cn/errors/application.ts +++ b/packages/phrases/src/locales/zh-cn/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/zh-hk/errors/application.ts b/packages/phrases/src/locales/zh-hk/errors/application.ts index 636b32293..4416a96dc 100644 --- a/packages/phrases/src/locales/zh-hk/errors/application.ts +++ b/packages/phrases/src/locales/zh-hk/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/packages/phrases/src/locales/zh-tw/errors/application.ts b/packages/phrases/src/locales/zh-tw/errors/application.ts index da89e6b32..dc6cf87f9 100644 --- a/packages/phrases/src/locales/zh-tw/errors/application.ts +++ b/packages/phrases/src/locales/zh-tw/errors/application.ts @@ -5,6 +5,11 @@ const application = { /** UNTRANSLATED */ invalid_third_party_application_type: 'Only traditional web applications can be marked as a third-party app.', + /** UNTRANSLATED */ + user_consent_scopes_only_for_third_party_applications: + 'Only third-party applications can manage user consent scopes.', + /** UNTRANSLATED */ + user_consent_scopes_not_found: 'Invalid user consent scopes.', }; export default Object.freeze(application); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be37feb11..ab9d848c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3747,6 +3747,9 @@ importers: '@logto/connector-kit': specifier: workspace:^2.0.0 version: link:../toolkit/connector-kit + '@logto/core-kit': + specifier: workspace:^ + version: link:../toolkit/core-kit '@logto/js': specifier: ^3.0.1 version: 3.0.1