diff --git a/.gitignore b/.gitignore index a95d6581e..761882df1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ dump.rdb # console auto generated files /packages/console/src/consts/jwt-customizer-type-definition.ts + +# Temporarily ignore SAML OpenAPI spec file (will be needed later) +packages/core/src/saml-applications/routes/index.openapi.json diff --git a/packages/core/package.json b/packages/core/package.json index 06906c241..3a4014e9b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,6 +80,7 @@ "ky": "^1.2.3", "lru-cache": "^11.0.0", "nanoid": "^5.0.1", + "node-forge": "^1.3.1", "oidc-provider": "^8.4.6", "openapi-types": "^12.1.3", "otplib": "^12.0.1", @@ -114,6 +115,7 @@ "@types/koa-send": "^4.1.3", "@types/koa__cors": "^5.0.0", "@types/node": "^20.9.5", + "@types/node-forge": "^1.3.1", "@types/oidc-provider": "^8.4.4", "@types/pluralize": "^0.0.33", "@types/qrcode": "^1.5.2", diff --git a/packages/core/src/routes/applications/application.ts b/packages/core/src/routes/applications/application.ts index a89f4535c..0cdc13215 100644 --- a/packages/core/src/routes/applications/application.ts +++ b/packages/core/src/routes/applications/application.ts @@ -152,7 +152,7 @@ export default function applicationRoutes( const { oidcClientMetadata, protectedAppMetadata, ...rest } = ctx.guard.body; if (rest.type === ApplicationType.SAML) { - throw new RequestError('application.use_saml_app_api'); + throw new RequestError('application.saml.use_saml_app_api'); } await Promise.all([ @@ -268,7 +268,7 @@ export default function applicationRoutes( const pendingUpdateApplication = await queries.applications.findApplicationById(id); if (pendingUpdateApplication.type === ApplicationType.SAML) { - throw new RequestError('application.use_saml_app_api'); + throw new RequestError('application.saml.use_saml_app_api'); } // @deprecated @@ -348,7 +348,7 @@ export default function applicationRoutes( const { type, protectedAppMetadata } = await queries.applications.findApplicationById(id); if (type === ApplicationType.SAML) { - throw new RequestError('application.use_saml_app_api'); + throw new RequestError('application.saml.use_saml_app_api'); } if (type === ApplicationType.Protected && protectedAppMetadata) { diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 0a35c96cc..48c0283f0 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -7,6 +7,7 @@ import koaAuditLog from '#src/middleware/koa-audit-log.js'; import koaBodyEtag from '#src/middleware/koa-body-etag.js'; import { koaManagementApiHooks } from '#src/middleware/koa-management-api-hooks.js'; import koaTenantGuard from '#src/middleware/koa-tenant-guard.js'; +import samlApplicationRoutes from '#src/saml-applications/routes/index.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import koaAuth from '../middleware/koa-auth/index.js'; @@ -99,6 +100,13 @@ const createRouters = (tenant: TenantContext) => { systemRoutes(managementRouter, tenant); subjectTokenRoutes(managementRouter, tenant); accountCentersRoutes(managementRouter, tenant); + // TODO: @darcy per our design, we will move related routes to Cloud repo and the routes will be loaded from remote. + if ( + (EnvSet.values.isDevFeaturesEnabled && EnvSet.values.isCloud) || + EnvSet.values.isIntegrationTest + ) { + samlApplicationRoutes(managementRouter, tenant); + } const anonymousRouter: AnonymousRouter = new Router(); diff --git a/packages/core/src/routes/swagger/utils/general.ts b/packages/core/src/routes/swagger/utils/general.ts index 4a6955d2d..12f3be596 100644 --- a/packages/core/src/routes/swagger/utils/general.ts +++ b/packages/core/src/routes/swagger/utils/general.ts @@ -138,9 +138,7 @@ const validateSupplementPaths = ( if ( isKeyInObject(operation, 'tags') && Array.isArray(operation.tags) && - !operation.tags.every( - (tag) => typeof tag === 'string' && [cloudOnlyTag, devFeatureTag].includes(tag) - ) + !operation.tags.every((tag) => typeof tag === 'string' && reservedTags.has(tag)) ) { throw new TypeError( `Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\` except for tag \`${cloudOnlyTag}\` and \`${devFeatureTag}\`. Define tags in the document root instead.` diff --git a/packages/core/src/routes/swagger/utils/operation.ts b/packages/core/src/routes/swagger/utils/operation.ts index 4a3b726b1..0a3e2b3e9 100644 --- a/packages/core/src/routes/swagger/utils/operation.ts +++ b/packages/core/src/routes/swagger/utils/operation.ts @@ -133,6 +133,9 @@ export const buildRouterObjects = (routers: T[], option router.stack // Filter out universal routes (mostly like a proxy route to withtyped) .filter(({ path }) => !path.includes('.*')) + // TODO: Remove this and bring back `/saml-applications` routes before release. + // Exclude `/saml-applications` routes for now. + .filter(({ path }) => !path.startsWith('/saml-applications')) .flatMap(({ path: routerPath, stack, methods }) => methods .map((method) => method.toLowerCase()) diff --git a/packages/core/src/saml-applications/libraries/secrets.ts b/packages/core/src/saml-applications/libraries/secrets.ts new file mode 100644 index 000000000..a18378388 --- /dev/null +++ b/packages/core/src/saml-applications/libraries/secrets.ts @@ -0,0 +1,34 @@ +import { generateStandardId } from '@logto/shared'; + +import type Queries from '#src/tenants/Queries.js'; + +import { generateKeyPairAndCertificate } from './utils.js'; + +export const createSamlApplicationSecretsLibrary = (queries: Queries) => { + const { + samlApplicationSecrets: { insertSamlApplicationSecret }, + } = queries; + + const createSamlApplicationSecret = async ( + applicationId: string, + // Set certificate life span to 1 year by default. + lifeSpanInDays = 365 + ) => { + const { privateKey, certificate, notAfter } = await generateKeyPairAndCertificate( + lifeSpanInDays + ); + + return insertSamlApplicationSecret({ + id: generateStandardId(), + applicationId, + privateKey, + certificate, + expiresAt: Math.floor(notAfter.getTime() / 1000), + active: false, + }); + }; + + return { + createSamlApplicationSecret, + }; +}; diff --git a/packages/core/src/saml-applications/libraries/utils.test.ts b/packages/core/src/saml-applications/libraries/utils.test.ts new file mode 100644 index 000000000..90e25e6af --- /dev/null +++ b/packages/core/src/saml-applications/libraries/utils.test.ts @@ -0,0 +1,59 @@ +import { addDays } from 'date-fns'; +import forge from 'node-forge'; + +import { generateKeyPairAndCertificate } from './utils.js'; + +describe('generateKeyPairAndCertificate', () => { + it('should generate valid key pair and certificate', async () => { + const result = await generateKeyPairAndCertificate(); + + // Verify private key format + expect(result.privateKey).toContain('-----BEGIN RSA PRIVATE KEY-----'); + expect(result.privateKey).toContain('-----END RSA PRIVATE KEY-----'); + + // Verify certificate format + expect(result.certificate).toContain('-----BEGIN CERTIFICATE-----'); + expect(result.certificate).toContain('-----END CERTIFICATE-----'); + + // Verify expiration date (default 365 days) + const expectedNotAfter = addDays(new Date(), 365); + expect(result.notAfter.getDate()).toBe(expectedNotAfter.getDate()); + expect(result.notAfter.getMonth()).toBe(expectedNotAfter.getMonth()); + expect(result.notAfter.getFullYear()).toBe(expectedNotAfter.getFullYear()); + + // Verify certificate content + const cert = forge.pki.certificateFromPem(result.certificate); + expect(cert.subject.getField('CN').value).toBe('example.com'); + expect(cert.issuer.getField('CN').value).toBe('logto.io'); + expect(cert.issuer.getField('O').value).toBe('Logto'); + expect(cert.issuer.getField('C').value).toBe('US'); + }); + + it('should generate certificate with custom lifespan', async () => { + const customDays = 30; + const result = await generateKeyPairAndCertificate(customDays); + + const expectedNotAfter = addDays(new Date(), customDays); + expect(result.notAfter.getDate()).toBe(expectedNotAfter.getDate()); + expect(result.notAfter.getMonth()).toBe(expectedNotAfter.getMonth()); + expect(result.notAfter.getFullYear()).toBe(expectedNotAfter.getFullYear()); + }); + + it('should generate unique serial numbers for different certificates', async () => { + const result1 = await generateKeyPairAndCertificate(); + const result2 = await generateKeyPairAndCertificate(); + + const cert1 = forge.pki.certificateFromPem(result1.certificate); + const cert2 = forge.pki.certificateFromPem(result2.certificate); + + expect(cert1.serialNumber).not.toBe(cert2.serialNumber); + }); + + it('should generate RSA key pair with 4096 bits', async () => { + const result = await generateKeyPairAndCertificate(); + const privateKey = forge.pki.privateKeyFromPem(result.privateKey); + + // RSA key should be 4096 bits + expect(forge.pki.privateKeyToPem(privateKey).length).toBeGreaterThan(3000); // A 4096-bit RSA private key in PEM format is typically longer than 3000 characters + }); +}); diff --git a/packages/core/src/saml-applications/libraries/utils.ts b/packages/core/src/saml-applications/libraries/utils.ts new file mode 100644 index 000000000..e73621394 --- /dev/null +++ b/packages/core/src/saml-applications/libraries/utils.ts @@ -0,0 +1,58 @@ +import crypto from 'node:crypto'; + +import { addDays } from 'date-fns'; +import forge from 'node-forge'; + +export const generateKeyPairAndCertificate = async (lifeSpanInDays = 365) => { + const keypair = forge.pki.rsa.generateKeyPair({ bits: 4096 }); + return createCertificate(keypair, lifeSpanInDays); +}; + +const createCertificate = (keypair: forge.pki.KeyPair, lifeSpanInDays: number) => { + const cert = forge.pki.createCertificate(); + const notBefore = new Date(); + const notAfter = addDays(notBefore, lifeSpanInDays); + + // Can not initialize the certificate with the keypair directly, so we need to set the public key manually. + /* eslint-disable @silverhand/fp/no-mutation */ + cert.publicKey = keypair.publicKey; + // Use cryptographically secure pseudorandom number generator (CSPRNG) to generate a random serial number (usually more than 8 bytes). + // `serialNumber` should be IDENTICAL across different certificates, better not to be incremental. + cert.serialNumber = crypto.randomBytes(16).toString('hex'); + cert.validity.notBefore = notBefore; + cert.validity.notAfter = notAfter; + /* eslint-enable @silverhand/fp/no-mutation */ + + // TODO: read from tenant config or let user customize before downloading + const subjectAttributes: forge.pki.CertificateField[] = [ + { + name: 'commonName', + value: 'example.com', + }, + ]; + + const issuerAttributes: forge.pki.CertificateField[] = [ + { + name: 'commonName', + value: 'logto.io', + }, + { + name: 'organizationName', + value: 'Logto', + }, + { + name: 'countryName', + value: 'US', + }, + ]; + + cert.setSubject(subjectAttributes); + cert.setIssuer(issuerAttributes); + cert.sign(keypair.privateKey); + + return { + privateKey: forge.pki.privateKeyToPem(keypair.privateKey), + certificate: forge.pki.certificateToPem(cert), + notAfter, + }; +}; diff --git a/packages/core/src/saml-applications/queries/configs.ts b/packages/core/src/saml-applications/queries/configs.ts new file mode 100644 index 000000000..f95486c20 --- /dev/null +++ b/packages/core/src/saml-applications/queries/configs.ts @@ -0,0 +1,30 @@ +import { type SamlApplicationConfig, SamlApplicationConfigs } from '@logto/schemas'; +import type { CommonQueryMethods } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; + +import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; +import { buildUpdateWhereWithPool } from '#src/database/update-where.js'; +import { convertToIdentifiers } from '#src/utils/sql.js'; + +const { table, fields } = convertToIdentifiers(SamlApplicationConfigs); + +export const createSamlApplicationConfigQueries = (pool: CommonQueryMethods) => { + const insertSamlApplicationConfig = buildInsertIntoWithPool(pool)(SamlApplicationConfigs, { + returning: true, + }); + + const updateSamlApplicationConfig = buildUpdateWhereWithPool(pool)(SamlApplicationConfigs, true); + + const findSamlApplicationConfigByApplicationId = async (applicationId: string) => + pool.maybeOne(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.applicationId}=${applicationId} + `); + + return { + insertSamlApplicationConfig, + updateSamlApplicationConfig, + findSamlApplicationConfigByApplicationId, + }; +}; diff --git a/packages/core/src/saml-applications/queries/secrets.ts b/packages/core/src/saml-applications/queries/secrets.ts new file mode 100644 index 000000000..7101b59be --- /dev/null +++ b/packages/core/src/saml-applications/queries/secrets.ts @@ -0,0 +1,39 @@ +import { SamlApplicationSecrets, type SamlApplicationSecret } from '@logto/schemas'; +import type { CommonQueryMethods } from '@silverhand/slonik'; +import { sql } from '@silverhand/slonik'; + +import { buildInsertIntoWithPool } from '#src/database/insert-into.js'; +import { DeletionError } from '#src/errors/SlonikError/index.js'; +import { convertToIdentifiers } from '#src/utils/sql.js'; + +const { table, fields } = convertToIdentifiers(SamlApplicationSecrets); + +export const createSamlApplicationSecretsQueries = (pool: CommonQueryMethods) => { + const insertSamlApplicationSecret = buildInsertIntoWithPool(pool)(SamlApplicationSecrets, { + returning: true, + }); + + const findSamlApplicationSecretsByApplicationId = async (applicationId: string) => + pool.any(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.applicationId}=${applicationId} + `); + + const deleteSamlApplicationSecretById = async (id: string) => { + const { rowCount } = await pool.query(sql` + delete from ${table} + where ${fields.id} = ${id} + `); + + if (rowCount < 1) { + throw new DeletionError(SamlApplicationSecrets.table); + } + }; + + return { + insertSamlApplicationSecret, + findSamlApplicationSecretsByApplicationId, + deleteSamlApplicationSecretById, + }; +}; diff --git a/packages/core/src/saml-applications/routes/index.ts b/packages/core/src/saml-applications/routes/index.ts new file mode 100644 index 000000000..1d0486614 --- /dev/null +++ b/packages/core/src/saml-applications/routes/index.ts @@ -0,0 +1,94 @@ +import { + ApplicationType, + samlApplicationCreateGuard, + samlApplicationResponseGuard, +} from '@logto/schemas'; +import { generateStandardId } from '@logto/shared'; +import { removeUndefinedKeys } from '@silverhand/essentials'; +import { z } from 'zod'; + +import koaGuard from '#src/middleware/koa-guard.js'; +import { buildOidcClientMetadata } from '#src/oidc/utils.js'; +import { generateInternalSecret } from '#src/routes/applications/application-secret.js'; +import type { ManagementApiRouter, RouterInitArgs } from '#src/routes/types.js'; +import { ensembleSamlApplication, validateAcsUrl } from '#src/saml-applications/routes/utils.js'; +import assertThat from '#src/utils/assert-that.js'; + +export default function samlApplicationRoutes( + ...[router, { queries, libraries }]: RouterInitArgs +) { + const { + applications: { insertApplication, findApplicationById, deleteApplicationById }, + samlApplicationConfigs: { insertSamlApplicationConfig }, + } = queries; + const { + samlApplicationSecrets: { createSamlApplicationSecret }, + } = libraries; + + router.post( + '/saml-applications', + koaGuard({ + body: samlApplicationCreateGuard, + response: samlApplicationResponseGuard, + status: [201, 400], + }), + async (ctx, next) => { + const { name, description, customData, config } = ctx.guard.body; + + if (config?.acsUrl) { + validateAcsUrl(config.acsUrl); + } + + const application = await insertApplication( + removeUndefinedKeys({ + id: generateStandardId(), + secret: generateInternalSecret(), + name, + description, + customData, + oidcClientMetadata: buildOidcClientMetadata(), + isThirdParty: true, + type: ApplicationType.SAML, + }) + ); + + try { + const [samlConfig, _] = await Promise.all([ + insertSamlApplicationConfig({ + applicationId: application.id, + ...config, + }), + createSamlApplicationSecret(application.id), + ]); + + ctx.status = 201; + ctx.body = ensembleSamlApplication({ application, samlConfig }); + } catch (error) { + await deleteApplicationById(application.id); + throw error; + } + + return next(); + } + ); + + router.delete( + '/saml-applications/:id', + koaGuard({ + params: z.object({ id: z.string() }), + status: [204, 400, 404], + }), + async (ctx, next) => { + const { id } = ctx.guard.params; + + const { type } = await findApplicationById(id); + assertThat(type === ApplicationType.SAML, 'application.saml.saml_application_only'); + + await deleteApplicationById(id); + + ctx.status = 204; + + return next(); + } + ); +} diff --git a/packages/core/src/saml-applications/routes/utils.ts b/packages/core/src/saml-applications/routes/utils.ts new file mode 100644 index 000000000..857a1378e --- /dev/null +++ b/packages/core/src/saml-applications/routes/utils.ts @@ -0,0 +1,42 @@ +import { + type SamlApplicationResponse, + type Application, + type SamlApplicationConfig, + type SamlAcsUrl, + BindingType, +} from '@logto/schemas'; + +import RequestError from '#src/errors/RequestError/index.js'; +import assertThat from '#src/utils/assert-that.js'; + +/** + * According to the design, a SAML app will be associated with multiple records from various tables. + * Therefore, when complete SAML app data is required, it is necessary to retrieve multiple related records and assemble them into a comprehensive SAML app dataset. This dataset includes: + * - A record from the `applications` table with a `type` of `SAML` + * - A record from the `saml_application_configs` table + */ +export const ensembleSamlApplication = ({ + application, + samlConfig, +}: { + application: Application; + samlConfig: Pick; +}): SamlApplicationResponse => { + return { + ...application, + ...samlConfig, + }; +}; + +/** + * Only HTTP-POST binding is supported for receiving SAML assertions at the moment. + */ +export const validateAcsUrl = (acsUrl: SamlAcsUrl) => { + assertThat( + acsUrl.binding === BindingType.POST, + new RequestError({ + code: 'application.saml.acs_url_binding_not_supported', + status: 422, + }) + ); +}; diff --git a/packages/core/src/tenants/Libraries.ts b/packages/core/src/tenants/Libraries.ts index 989db236b..24b7e5f1b 100644 --- a/packages/core/src/tenants/Libraries.ts +++ b/packages/core/src/tenants/Libraries.ts @@ -17,6 +17,7 @@ import { createSocialLibrary } from '#src/libraries/social.js'; import { createSsoConnectorLibrary } from '#src/libraries/sso-connector.js'; import { createUserLibrary } from '#src/libraries/user.js'; import { createVerificationStatusLibrary } from '#src/libraries/verification-status.js'; +import { createSamlApplicationSecretsLibrary } from '#src/saml-applications/libraries/secrets.js'; import type Queries from './Queries.js'; @@ -37,6 +38,7 @@ export default class Libraries { passcodes = createPasscodeLibrary(this.queries, this.connectors); applications = createApplicationLibrary(this.queries); verificationStatuses = createVerificationStatusLibrary(this.queries); + samlApplicationSecrets = createSamlApplicationSecretsLibrary(this.queries); roleScopes = createRoleScopeLibrary(this.queries); domains = createDomainLibrary(this.queries); protectedApps = createProtectedAppLibrary(this.queries); diff --git a/packages/core/src/tenants/Queries.ts b/packages/core/src/tenants/Queries.ts index 006de0db7..61de25720 100644 --- a/packages/core/src/tenants/Queries.ts +++ b/packages/core/src/tenants/Queries.ts @@ -28,6 +28,8 @@ import UserSsoIdentityQueries from '#src/queries/user-sso-identities.js'; import { createUserQueries } from '#src/queries/user.js'; import { createUsersRolesQueries } from '#src/queries/users-roles.js'; import { createVerificationStatusQueries } from '#src/queries/verification-status.js'; +import { createSamlApplicationConfigQueries } from '#src/saml-applications/queries/configs.js'; +import { createSamlApplicationSecretsQueries } from '#src/saml-applications/queries/secrets.js'; import { AccountCenterQueries } from '../queries/account-center.js'; import { PersonalAccessTokensQueries } from '../queries/personal-access-tokens.js'; @@ -60,6 +62,8 @@ export default class Queries { ssoConnectors = new SsoConnectorQueries(this.pool); userSsoIdentities = new UserSsoIdentityQueries(this.pool); subjectTokens = createSubjectTokenQueries(this.pool); + samlApplicationSecrets = createSamlApplicationSecretsQueries(this.pool); + samlApplicationConfigs = createSamlApplicationConfigQueries(this.pool); personalAccessTokens = new PersonalAccessTokensQueries(this.pool); verificationRecords = new VerificationRecordQueries(this.pool); accountCenters = new AccountCenterQueries(this.pool); diff --git a/packages/integration-tests/src/api/saml-application.ts b/packages/integration-tests/src/api/saml-application.ts new file mode 100644 index 000000000..88b2ff88d --- /dev/null +++ b/packages/integration-tests/src/api/saml-application.ts @@ -0,0 +1,13 @@ +import { type SamlApplicationResponse, type CreateSamlApplication } from '@logto/schemas'; + +import { authedAdminApi } from './api.js'; + +export const createSamlApplication = async (createSamlApplication: CreateSamlApplication) => + authedAdminApi + .post('saml-applications', { + json: createSamlApplication, + }) + .json(); + +export const deleteSamlApplication = async (id: string) => + authedAdminApi.delete(`saml-applications/${id}`); diff --git a/packages/integration-tests/src/tests/api/application/application.test.ts b/packages/integration-tests/src/tests/api/application/application.test.ts index 2d8d6446b..7873ab46a 100644 --- a/packages/integration-tests/src/tests/api/application/application.test.ts +++ b/packages/integration-tests/src/tests/api/application/application.test.ts @@ -37,7 +37,7 @@ describe('application APIs', () => { it('should throw error when creating a SAML application', async () => { await expectRejects(createApplication('test-create-saml-app', ApplicationType.SAML), { - code: 'application.use_saml_app_api', + code: 'application.saml.use_saml_app_api', status: 400, }); }); @@ -54,6 +54,7 @@ describe('application APIs', () => { expect(application.name).toBe(applicationName); expect(application.type).toBe(ApplicationType.Traditional); expect(application.isThirdParty).toBe(true); + await deleteApplication(application.id); }); diff --git a/packages/integration-tests/src/tests/api/application/saml-application.test.ts b/packages/integration-tests/src/tests/api/application/saml-application.test.ts new file mode 100644 index 000000000..e990dd28e --- /dev/null +++ b/packages/integration-tests/src/tests/api/application/saml-application.test.ts @@ -0,0 +1,68 @@ +import { ApplicationType, BindingType } from '@logto/schemas'; + +import { createApplication, deleteApplication } from '#src/api/application.js'; +import { createSamlApplication, deleteSamlApplication } from '#src/api/saml-application.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { devFeatureTest } from '#src/utils.js'; + +const { it, describe } = devFeatureTest; + +describe('SAML application', () => { + it('should create and delete a SAML application successfully', async () => { + const createdSamlApplication = await createSamlApplication({ + name: 'test', + description: 'test', + }); + + await deleteSamlApplication(createdSamlApplication.id); + }); + + it('should not support HTTP-Redirect binding', async () => { + await expectRejects( + createSamlApplication({ + name: 'test', + description: 'test', + config: { + acsUrl: { + binding: BindingType.REDIRECT, + url: 'https://example.com', + }, + }, + }), + { + code: 'application.saml.acs_url_binding_not_supported', + status: 422, + } + ); + }); + + it('should be able to create SAML application with `config` field', async () => { + const config = { + entityId: 'https://example.logto.io', + acsUrl: { + binding: BindingType.POST, + url: 'https://example.logto.io/sso/saml', + }, + }; + const createdSamlApplication = await createSamlApplication({ + name: 'test', + description: 'test', + config, + }); + expect(createdSamlApplication.entityId).toEqual(config.entityId); + expect(createdSamlApplication.acsUrl).toEqual(config.acsUrl); + await deleteSamlApplication(createdSamlApplication.id); + }); + + it('can not delete non-SAML applications with `DEL /saml-applications/:id` API', async () => { + const application = await createApplication('test-non-saml-app', ApplicationType.Traditional, { + isThirdParty: true, + }); + + await expectRejects(deleteSamlApplication(application.id), { + code: 'application.saml.saml_application_only', + status: 400, + }); + await deleteApplication(application.id); + }); +}); diff --git a/packages/phrases/src/locales/en/errors/application.ts b/packages/phrases/src/locales/en/errors/application.ts index ca715ecb5..7a4dc2960 100644 --- a/packages/phrases/src/locales/en/errors/application.ts +++ b/packages/phrases/src/locales/en/errors/application.ts @@ -10,7 +10,6 @@ const application = { protected_app_metadata_is_required: 'Protected app metadata is required.', protected_app_not_configured: 'Protected app provider is not configured. This feature is not available for open source version.', - use_saml_app_api: 'Use `[METHOD] /saml-applications(/.*)?` API to operate SAML app.', cloudflare_unknown_error: 'Got unknown error when requesting Cloudflare API', protected_application_only: 'The feature is only available for protected applications.', protected_application_misconfigured: 'Protected application is misconfigured.', @@ -21,6 +20,12 @@ const application = { should_delete_custom_domains_first: 'Should delete custom domains first.', no_legacy_secret_found: 'The application does not have a legacy secret.', secret_name_exists: 'Secret name already exists.', + saml: { + use_saml_app_api: 'Use `[METHOD] /saml-applications(/.*)?` API to operate SAML app.', + saml_application_only: 'The API is only available for SAML applications.', + acs_url_binding_not_supported: + 'Only HTTP-POST binding is supported for receiving SAML assertions.', + }, }; export default Object.freeze(application); diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index 6806a3ca3..5172915b8 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -30,3 +30,4 @@ export * from './onboarding.js'; export * from './sign-in-experience.js'; export * from './subject-token.js'; export * from './ssr.js'; +export * from './saml-application.js'; diff --git a/packages/schemas/src/types/saml-application.ts b/packages/schemas/src/types/saml-application.ts new file mode 100644 index 000000000..10b962ea3 --- /dev/null +++ b/packages/schemas/src/types/saml-application.ts @@ -0,0 +1,33 @@ +import { type z } from 'zod'; + +import { Applications } from '../db-entries/application.js'; +import { SamlApplicationConfigs } from '../db-entries/saml-application-config.js'; + +import { applicationCreateGuard } from './application.js'; + +const samlAppConfigGuard = SamlApplicationConfigs.guard.pick({ + attributeMapping: true, + entityId: true, + acsUrl: true, +}); + +export const samlApplicationCreateGuard = applicationCreateGuard + .pick({ + name: true, + description: true, + customData: true, + }) + .extend({ + // The reason for encapsulating attributeMapping and spMetadata into an object within the config field is that you cannot provide only one of `attributeMapping` or `spMetadata`. Due to the structure of the `saml_application_configs` table, both must be not null. + config: samlAppConfigGuard.partial().optional(), + }); + +export type CreateSamlApplication = z.infer; + +export const samlApplicationResponseGuard = Applications.guard.merge( + // Partial to allow the optional fields to be omitted in the response. + // When starting to create a SAML application, SAML configuration is optional, which can lead to the absence of SAML configuration. + samlAppConfigGuard +); + +export type SamlApplicationResponse = z.infer; diff --git a/packages/schemas/tables/saml_application_configs.sql b/packages/schemas/tables/saml_application_configs.sql index 7ff2a859b..0af07aca9 100644 --- a/packages/schemas/tables/saml_application_configs.sql +++ b/packages/schemas/tables/saml_application_configs.sql @@ -1,10 +1,6 @@ /* init_order = 2 */ -/** - * The SAML application config and SAML-type application have a one-to-one correspondence: - * - a SAML-type application can only have one SAML application config - * - a SAML application config can only configure one SAML-type application - */ +/** The SAML application config and SAML-type application have a one-to-one correspondence: 1. a SAML-type application can only have one SAML application config. (CANNOT use "semicolon" in comments, since it indicates the end of query.) 2. a SAML application config can only configure one SAML-type application. */ create table saml_application_configs ( application_id varchar(21) not null references applications (id) on update cascade on delete cascade, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e18d4ef62..403ff173b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 8.8.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.0.2)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.0.2)(yaml@2.4.5) typescript: specifier: ^5.0.0 version: 5.0.2 @@ -266,7 +266,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -333,7 +333,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -391,7 +391,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -449,7 +449,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -513,7 +513,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -577,7 +577,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -638,7 +638,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -705,7 +705,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -763,7 +763,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -821,7 +821,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -879,7 +879,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -937,7 +937,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -998,7 +998,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1068,7 +1068,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1129,7 +1129,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1184,7 +1184,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1242,7 +1242,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1300,7 +1300,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1358,7 +1358,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1419,7 +1419,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1477,7 +1477,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1535,7 +1535,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1593,7 +1593,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1651,7 +1651,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1709,7 +1709,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1767,7 +1767,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1825,7 +1825,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1883,7 +1883,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -1950,7 +1950,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2020,7 +2020,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2078,7 +2078,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2133,7 +2133,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2197,7 +2197,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2255,7 +2255,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2313,7 +2313,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2377,7 +2377,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2435,7 +2435,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2493,7 +2493,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2548,7 +2548,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2606,7 +2606,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2664,7 +2664,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -2722,7 +2722,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -3182,6 +3182,9 @@ importers: nanoid: specifier: ^5.0.1 version: 5.0.1 + node-forge: + specifier: ^1.3.1 + version: 1.3.1 oidc-provider: specifier: ^8.4.6 version: 8.4.6 @@ -3279,6 +3282,9 @@ importers: '@types/node': specifier: ^20.9.5 version: 20.10.4 + '@types/node-forge': + specifier: ^1.3.1 + version: 1.3.11 '@types/oidc-provider': specifier: ^8.4.4 version: 8.4.4 @@ -3305,7 +3311,7 @@ importers: version: 8.57.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -3335,7 +3341,7 @@ importers: version: 7.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -3504,7 +3510,7 @@ importers: version: 3.0.0 tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) packages/experience: devDependencies: @@ -3979,7 +3985,7 @@ importers: version: 10.0.0 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) jest-matcher-specific-error: specifier: ^1.0.0 version: 1.0.0 @@ -4006,7 +4012,7 @@ importers: version: 22.6.5(typescript@5.5.3) tsup: specifier: ^8.3.0 - version: 8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) + version: 8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5) typescript: specifier: ^5.5.3 version: 5.5.3 @@ -7088,6 +7094,9 @@ packages: '@types/node-fetch@2.6.2': resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -15868,7 +15877,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -15882,7 +15891,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -15903,7 +15912,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -15917,7 +15926,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -17641,6 +17650,10 @@ snapshots: '@types/node': 20.12.7 form-data: 3.0.2 + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 20.12.7 + '@types/node@12.20.55': {} '@types/node@20.10.4': @@ -19305,13 +19318,13 @@ snapshots: dependencies: lodash.get: 4.4.2 - create-jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)): + create-jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -21725,16 +21738,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)): + jest-cli@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + create-jest: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + jest-config: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -21763,7 +21776,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)): + jest-config@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)): dependencies: '@babel/core': 7.24.4 '@jest/test-sequencer': 29.7.0 @@ -21789,38 +21802,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.10.4 - ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)): - dependencies: - '@babel/core': 7.24.4 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.24.4) - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.12.7 - ts-node: 10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3) + ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -21856,6 +21838,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.12.7)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)): + dependencies: + '@babel/core': 7.24.4 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.4) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.12.7 + ts-node: 10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-dev-server@10.1.1: dependencies: chalk: 4.1.2 @@ -22164,12 +22177,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)): + jest@29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3)) + jest-cli: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -25819,14 +25832,14 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.10.4)(typescript@5.5.3): + ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.10.4 + '@types/node': 20.12.7 acorn: 8.13.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -25840,14 +25853,14 @@ snapshots: '@swc/core': 1.3.52(@swc/helpers@0.5.1) optional: true - ts-node@10.9.2(@swc/core@1.3.52(@swc/helpers@0.5.1))(@types/node@20.12.7)(typescript@5.5.3): + ts-node@10.9.2(@swc/core@1.3.52)(@types/node@20.10.4)(typescript@5.5.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.12.7 + '@types/node': 20.10.4 acorn: 8.13.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -25884,7 +25897,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.0.2)(yaml@2.4.5): + tsup@8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.0.2)(yaml@2.4.5): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) cac: 6.7.14 @@ -25912,7 +25925,7 @@ snapshots: - tsx - yaml - tsup@8.3.0(@swc/core@1.3.52(@swc/helpers@0.5.1))(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5): + tsup@8.3.0(@swc/core@1.3.52)(jiti@1.21.0)(postcss@8.4.39)(typescript@5.5.3)(yaml@2.4.5): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) cac: 6.7.14