diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index aed3fbade..586f05126 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -118,7 +118,7 @@ jobs: - name: Setup mock Cloudflare Hostname Provider config working-directory: tests - run: npm run cli db system set cloudflareHostnameProvider '{"zoneId":"mock-zone-id","apiToken":"","fallbackOrigin":"mock.logto.dev"}' + run: npm run cli db system set cloudflareHostnameProvider '{"zoneId":"mock-zone-id","apiToken":""}' env: DB_URL: postgres://postgres:postgres@localhost:5432/postgres diff --git a/packages/core/src/__mocks__/system.ts b/packages/core/src/__mocks__/system.ts index 9e2e315cf..03cbfb1c2 100644 --- a/packages/core/src/__mocks__/system.ts +++ b/packages/core/src/__mocks__/system.ts @@ -7,5 +7,4 @@ export const mockStorageProviderData = { export const mockHostnameProviderData = { apiToken: 'api-token', zoneId: 'zone-id', - fallbackOrigin: 'logto.com', }; diff --git a/packages/core/src/libraries/domain.test.ts b/packages/core/src/libraries/domain.test.ts index 936375360..d397bd57d 100644 --- a/packages/core/src/libraries/domain.test.ts +++ b/packages/core/src/libraries/domain.test.ts @@ -10,6 +10,7 @@ import { } from '#src/__mocks__/domain.js'; import RequestError from '#src/errors/RequestError/index.js'; import SystemContext from '#src/tenants/SystemContext.js'; +import { mockFallbackOrigin } from '#src/utils/cloudflare/mock.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); @@ -20,6 +21,7 @@ const { getCustomHostname, createCustomHostname, deleteCustomHostname } = mockEs createCustomHostname: jest.fn(async () => mockCloudflareData), getCustomHostname: jest.fn(async () => mockCloudflareData), deleteCustomHostname: jest.fn(), + getFallbackOrigin: jest.fn(async () => mockFallbackOrigin), }) ); @@ -34,13 +36,11 @@ const { syncDomainStatus, addDomain, deleteDomain } = createDomainLibrary( new MockQueries({ domains: { updateDomainById, insertDomain, findDomainById, deleteDomainById } }) ); -const fallbackOrigin = 'fake_origin'; beforeAll(() => { // eslint-disable-next-line @silverhand/fp/no-mutation SystemContext.shared.hostnameProviderConfig = { zoneId: 'fake_zone_id', apiToken: '', - fallbackOrigin, }; }); @@ -58,7 +58,7 @@ describe('addDomain()', () => { expect(response.dnsRecords).toContainEqual({ type: 'CNAME', name: mockDomainWithCloudflareData.domain, - value: fallbackOrigin, + value: mockFallbackOrigin, }); }); }); diff --git a/packages/core/src/libraries/domain.ts b/packages/core/src/libraries/domain.ts index 49e6c323f..5541fa9bf 100644 --- a/packages/core/src/libraries/domain.ts +++ b/packages/core/src/libraries/domain.ts @@ -8,6 +8,7 @@ import { getCustomHostname, createCustomHostname, deleteCustomHostname, + getFallbackOrigin, } from '#src/utils/cloudflare/index.js'; export type DomainLibrary = ReturnType; @@ -66,7 +67,10 @@ export const createDomainLibrary = (queries: Queries) => { const { hostnameProviderConfig } = SystemContext.shared; assertThat(hostnameProviderConfig, 'domain.not_configured'); - const cloudflareData = await createCustomHostname(hostnameProviderConfig, hostname); + const [fallbackOrigin, cloudflareData] = await Promise.all([ + getFallbackOrigin(hostnameProviderConfig), + createCustomHostname(hostnameProviderConfig, hostname), + ]); return insertDomain({ domain: hostname, @@ -74,11 +78,10 @@ export const createDomainLibrary = (queries: Queries) => { cloudflareData, status: DomainStatus.PendingVerification, dnsRecords: [ - // Verification CNAME, fixed value, generated by us { type: 'CNAME', name: hostname, - value: hostnameProviderConfig.fallbackOrigin, + value: fallbackOrigin, }, ], }); diff --git a/packages/core/src/utils/cloudflare/index.ts b/packages/core/src/utils/cloudflare/index.ts index 3b90c8b91..232b9bb52 100644 --- a/packages/core/src/utils/cloudflare/index.ts +++ b/packages/core/src/utils/cloudflare/index.ts @@ -1,16 +1,76 @@ import path from 'node:path'; import { type HostnameProviderData, cloudflareDataGuard } from '@logto/schemas'; -import { got } from 'got'; +import { type Response, got } from 'got'; +import { type ZodType } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '../assert-that.js'; import { baseUrl } from './consts.js'; -import { mockCustomHostnameResponse } from './mock.js'; +import { mockCustomHostnameResponse, mockFallbackOrigin } from './mock.js'; +import { cloudflareHostnameResponseGuard } from './types.js'; import { parseCloudflareResponse } from './utils.js'; +type HandleResponse = { + (response: Response, guard: ZodType): T; + (response: Response): void; +}; + +const handleResponse: HandleResponse = (response: Response, guard?: ZodType) => { + if (!response.ok) { + if (response.statusCode === 409) { + throw new RequestError('domain.hostname_already_exists'); + } + + throw new RequestError( + { + code: 'domain.cloudflare_unknown_error', + status: 500, + }, + response.body + ); + } + + if (!guard) { + return; + } + + const result = guard.safeParse(parseCloudflareResponse(response.body)); + + assertThat(result.success, 'domain.cloudflare_response_error'); + + return result.data; +}; + +export const getFallbackOrigin = async (auth: HostnameProviderData): Promise => { + const { + EnvSet: { + values: { isIntegrationTest }, + }, + } = await import('#src/env-set/index.js'); + if (isIntegrationTest) { + return mockFallbackOrigin; + } + + const response = await got.get( + new URL( + path.join(baseUrl.pathname, `/zones/${auth.zoneId}/custom_hostnames/fallback_origin`), + baseUrl + ), + { + headers: { + Authorization: `Bearer ${auth.apiToken}`, + }, + throwHttpErrors: false, + } + ); + + const result = handleResponse(response, cloudflareHostnameResponseGuard); + return result.origin; +}; + export const createCustomHostname = async (auth: HostnameProviderData, hostname: string) => { const { EnvSet: { @@ -35,25 +95,7 @@ export const createCustomHostname = async (auth: HostnameProviderData, hostname: } ); - if (!response.ok) { - if (response.statusCode === 409) { - throw new RequestError('domain.hostname_already_exists'); - } - - throw new RequestError( - { - code: 'domain.cloudflare_unknown_error', - status: 500, - }, - response.body - ); - } - - const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body)); - - assertThat(result.success, 'domain.cloudflare_response_error'); - - return result.data; + return handleResponse(response, cloudflareDataGuard); }; export const getCustomHostname = async (auth: HostnameProviderData, identifier: string) => { @@ -79,22 +121,7 @@ export const getCustomHostname = async (auth: HostnameProviderData, identifier: } ); - assertThat( - response.ok, - new RequestError( - { - code: 'domain.cloudflare_unknown_error', - status: 500, - }, - response.body - ) - ); - - const result = cloudflareDataGuard.safeParse(parseCloudflareResponse(response.body)); - - assertThat(result.success, 'domain.cloudflare_response_error'); - - return result.data; + return handleResponse(response, cloudflareDataGuard); }; export const deleteCustomHostname = async (auth: HostnameProviderData, identifier: string) => { @@ -120,14 +147,5 @@ export const deleteCustomHostname = async (auth: HostnameProviderData, identifie } ); - assertThat( - response.ok, - new RequestError( - { - code: 'domain.cloudflare_unknown_error', - status: 500, - }, - response.body - ) - ); + handleResponse(response); }; diff --git a/packages/core/src/utils/cloudflare/mock.ts b/packages/core/src/utils/cloudflare/mock.ts index 88a8b7adc..fbb836311 100644 --- a/packages/core/src/utils/cloudflare/mock.ts +++ b/packages/core/src/utils/cloudflare/mock.ts @@ -3,3 +3,5 @@ import { mockCloudflareData } from '#src/__mocks__/domain.js'; export const mockCustomHostnameResponse = async (identifier?: string) => { return mockCloudflareData; }; + +export const mockFallbackOrigin = 'mock.logto.dev'; diff --git a/packages/core/src/utils/cloudflare/types.ts b/packages/core/src/utils/cloudflare/types.ts index a870da425..2183cf2e0 100644 --- a/packages/core/src/utils/cloudflare/types.ts +++ b/packages/core/src/utils/cloudflare/types.ts @@ -4,3 +4,9 @@ export const cloudflareResponseGuard = z.object({ success: z.boolean(), result: z.unknown(), }); + +export const cloudflareHostnameResponseGuard = z + .object({ + origin: z.string(), + }) + .catchall(z.unknown()); diff --git a/packages/schemas/src/types/system.ts b/packages/schemas/src/types/system.ts index 789bdbe9c..4971b9f73 100644 --- a/packages/schemas/src/types/system.ts +++ b/packages/schemas/src/types/system.ts @@ -101,7 +101,6 @@ export const demoSocialGuard: Readonly<{ export const hostnameProviderDataGuard = z.object({ zoneId: z.string(), apiToken: z.string(), // Requires zone permission for "SSL and Certificates Edit" - fallbackOrigin: z.string(), // A domain name }); export type HostnameProviderData = z.infer;