From a4c9a8bbbc534bbd8e173f4bc361816e0ed44cd1 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 13 Nov 2026 05:45:21 -0800 Subject: [PATCH] feat(main): build-time service URL defaults for packaged builds (#667) Auth API, relay director, push gateway, connector and artifacts URLs can now be set per build (ORCA_BUILD_CLOUD_API_URL, ORCA_BUILD_RELAY_URL, ORCA_BUILD_PUSH_GATEWAY_URL, ORCA_BUILD_CONNECTOR_URL, ORCA_BUILD_ARTIFACTS_API_URL, or the matching keys in config/cloud-build-defaults.json). Unset keys keep today's built-in defaults, and dev run-time env overrides still win. No values change. --- config/build-plugins/cloud-build-defaults.ts | 69 +++++++++++++++- config/cloud-build-defaults.json | 2 +- src/main/artifacts/artifact-cloud-config.ts | 5 +- src/main/build-service-url-defaults.test.ts | 78 +++++++++++++++++++ src/main/cloud-build-defaults.test.ts | 20 ++++- src/main/cloud-build-defaults.ts | 27 +++++-- src/main/connector-cloud/connector-origin.ts | 4 +- .../cloud-auth/profile-cloud-auth-config.ts | 6 +- src/main/runtime/push/push-gateway-origin.ts | 6 +- 9 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 src/main/build-service-url-defaults.test.ts diff --git a/config/build-plugins/cloud-build-defaults.ts b/config/build-plugins/cloud-build-defaults.ts index d3c0769ee84..9ca03168759 100644 --- a/config/build-plugins/cloud-build-defaults.ts +++ b/config/build-plugins/cloud-build-defaults.ts @@ -1,13 +1,74 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' +type ServiceUrlKey = + | 'authApiUrl' + | 'relayDirectorUrl' + | 'pushGatewayUrl' + | 'connectorUrl' + | 'artifactsApiUrl' + +type BuildDefaultsFile = Partial> & { + billingApiUrl?: string + cloudSandboxUrl?: string + allowDevSigning?: boolean +} + +// Build-time service URL defaults; an unset key keeps the main-process literal default. +const SERVICE_URL_ENV: Record = { + authApiUrl: 'ORCA_BUILD_CLOUD_API_URL', + relayDirectorUrl: 'ORCA_BUILD_RELAY_URL', + pushGatewayUrl: 'ORCA_BUILD_PUSH_GATEWAY_URL', + connectorUrl: 'ORCA_BUILD_CONNECTOR_URL', + artifactsApiUrl: 'ORCA_BUILD_ARTIFACTS_API_URL' +} +const SERVICE_URL_KEYS: readonly ServiceUrlKey[] = [ + 'authApiUrl', + 'relayDirectorUrl', + 'pushGatewayUrl', + 'connectorUrl', + 'artifactsApiUrl' +] + +function serviceUrl(name: string, value: string | undefined): string | undefined { + const trimmed = value?.trim() + if (!trimmed) { + return undefined + } + // Fail the build rather than ship a default the app would reject at run time. + const url = URL.canParse(trimmed) ? new URL(trimmed) : null + if ( + !url || + url.protocol !== 'https:' || + url.username || + url.password || + url.search || + url.hash || + url.pathname !== '/' + ) { + throw new Error(`${name} must be an https origin`) + } + return url.origin +} + +function readDefaultsFile(): BuildDefaultsFile { + return JSON.parse(readFileSync(resolve('config/cloud-build-defaults.json'), 'utf8')) +} + /** Integration-branch backends baked into main (packaged builds read no shell env); build-time env overrides the file. */ -export function cloudBuildDefaultsDefine(env: NodeJS.ProcessEnv = process.env): string { - const file: { billingApiUrl?: string; cloudSandboxUrl?: string; allowDevSigning?: boolean } = - JSON.parse(readFileSync(resolve('config/cloud-build-defaults.json'), 'utf8')) +export function cloudBuildDefaultsDefine( + env: NodeJS.ProcessEnv = process.env, + file: BuildDefaultsFile = readDefaultsFile() +): string { + const serviceUrls: Partial> = {} + for (const key of SERVICE_URL_KEYS) { + const envName = SERVICE_URL_ENV[key] + serviceUrls[key] = serviceUrl(envName, env[envName] || file[key]) + } return JSON.stringify({ billingApiUrl: env.ORCA_BUILD_BILLING_API_URL || file.billingApiUrl, cloudSandboxUrl: env.ORCA_BUILD_CLOUD_SANDBOX_URL || file.cloudSandboxUrl, - allowDevSigning: file.allowDevSigning === true + allowDevSigning: file.allowDevSigning === true, + ...serviceUrls }) } diff --git a/config/cloud-build-defaults.json b/config/cloud-build-defaults.json index fcfbdefb2f2..a3489a05879 100644 --- a/config/cloud-build-defaults.json +++ b/config/cloud-build-defaults.json @@ -1,5 +1,5 @@ { - "_comment": "Integration-branch only (stablyai/orca-private cloud/integration). Public dev endpoints baked into packaged builds; no secrets. ORCA_BUILD_BILLING_API_URL / ORCA_BUILD_CLOUD_SANDBOX_URL at build time, or ORCA_BILLING_API_URL / ORCA_CLOUD_SANDBOX_URL at run time, override them. allowDevSigning lets this packaged build honor ORCA_CLOUD_SANDBOX_DEV_KEY_FILE (dev operator key) until login.onorca.dev ships the sandbox-token endpoint; the key itself is never in git.", + "_comment": "Integration-branch only (stablyai/orca-private cloud/integration). Public dev endpoints baked into packaged builds; no secrets. ORCA_BUILD_BILLING_API_URL / ORCA_BUILD_CLOUD_SANDBOX_URL at build time, or ORCA_BILLING_API_URL / ORCA_CLOUD_SANDBOX_URL at run time, override them. allowDevSigning lets this packaged build honor ORCA_CLOUD_SANDBOX_DEV_KEY_FILE (dev operator key) until login.onorca.dev ships the sandbox-token endpoint; the key itself is never in git. Optional build-time service URL defaults (authApiUrl, relayDirectorUrl, pushGatewayUrl, connectorUrl, artifactsApiUrl; or ORCA_BUILD_CLOUD_API_URL / ORCA_BUILD_RELAY_URL / ORCA_BUILD_PUSH_GATEWAY_URL / ORCA_BUILD_CONNECTOR_URL / ORCA_BUILD_ARTIFACTS_API_URL at build time) must be https origins; unset keys keep the built-in defaults.", "billingApiUrl": "https://orca-billing-dev.lovecast.workers.dev", "cloudSandboxUrl": "https://orca-sandbox-control-dev.lovecast.workers.dev", "allowDevSigning": true diff --git a/src/main/artifacts/artifact-cloud-config.ts b/src/main/artifacts/artifact-cloud-config.ts index 0333038c4ae..df188ab3c1a 100644 --- a/src/main/artifacts/artifact-cloud-config.ts +++ b/src/main/artifacts/artifact-cloud-config.ts @@ -1,4 +1,5 @@ import { app } from 'electron' +import { buildServiceUrlDefault } from '../cloud-build-defaults' const PRODUCTION_ARTIFACTS_API_URL = 'https://share.onorca.dev' @@ -16,7 +17,9 @@ export function resolveArtifactCloudApiUrl( packaged = isPackaged() ): string { const candidate = override?.trim() || env.ORCA_ARTIFACTS_API_URL?.trim() - const url = new URL(candidate || PRODUCTION_ARTIFACTS_API_URL) + const url = new URL( + candidate || buildServiceUrlDefault('artifactsApiUrl', PRODUCTION_ARTIFACTS_API_URL) + ) const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) const firstParty = ['onorca.dev', 'orca.dev'].some( (domain) => url.hostname === domain || url.hostname.endsWith(`.${domain}`) diff --git a/src/main/build-service-url-defaults.test.ts b/src/main/build-service-url-defaults.test.ts new file mode 100644 index 00000000000..9e0e6620025 --- /dev/null +++ b/src/main/build-service-url-defaults.test.ts @@ -0,0 +1,78 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ app: { isPackaged: false } })) + +import { resolveArtifactCloudApiUrl } from './artifacts/artifact-cloud-config' +import { resolveConnectorOrigin } from './connector-cloud/connector-origin' +import { getOrcaCloudAuthConfig } from './orca-profiles/cloud-auth/profile-cloud-auth-config' +import { resolvePushGatewayOrigin } from './runtime/push/push-gateway-origin' + +const authApiUrl = (env: NodeJS.ProcessEnv, packaged: boolean): string | null => { + const result = getOrcaCloudAuthConfig({ ORCA_CLOUD_CLIENT_ID: 'client', ...env }, packaged) + return result.configured ? result.config.apiBaseUrl : null +} +const relayDirectorUrl = (env: NodeJS.ProcessEnv, packaged: boolean): string | null => { + const devAuth = packaged ? {} : { ORCA_CLOUD_API_URL: 'http://127.0.0.1:8788' } + const result = getOrcaCloudAuthConfig( + { ORCA_CLOUD_CLIENT_ID: 'client', ...devAuth, ...env }, + packaged + ) + return result.configured ? result.config.relayDirectorUrl : null +} + +// Each row: build-default key, dev run-time env name, resolver, and today's packaged literal. +const cases = [ + { + key: 'authApiUrl', + envName: 'ORCA_CLOUD_API_URL', + resolve: authApiUrl, + literal: 'https://login.onorca.dev' + }, + { + key: 'relayDirectorUrl', + envName: 'ORCA_RELAY_URL', + resolve: relayDirectorUrl, + literal: 'https://relay.onorca.dev' + }, + { + key: 'pushGatewayUrl', + envName: 'ORCA_PUSH_GATEWAY_URL', + resolve: resolvePushGatewayOrigin, + literal: 'https://push.onorca.dev' + }, + { + key: 'connectorUrl', + envName: 'ORCA_CONNECTOR_URL', + resolve: resolveConnectorOrigin, + literal: 'https://connector.onorca.dev' + }, + { + key: 'artifactsApiUrl', + envName: 'ORCA_ARTIFACTS_API_URL', + resolve: (env: NodeJS.ProcessEnv, packaged: boolean) => + resolveArtifactCloudApiUrl(undefined, env, packaged), + literal: 'https://share.onorca.dev' + } +] + +describe('build-time service URL defaults', () => { + afterEach(() => vi.unstubAllGlobals()) + + describe.each(cases)('$key', ({ key, envName, resolve, literal }) => { + it('keeps the current default when the build sets none', () => { + expect(resolve({}, true)).toBe(literal) + vi.stubGlobal('ORCA_CLOUD_BUILD_DEFAULTS', { billingApiUrl: 'https://billing.example' }) + expect(resolve({}, true)).toBe(literal) + }) + + it('uses the build default when set', () => { + vi.stubGlobal('ORCA_CLOUD_BUILD_DEFAULTS', { [key]: 'https://build.onorca.dev' }) + expect(resolve({}, true)).toBe('https://build.onorca.dev') + }) + + it('lets the development run-time override win over the build default', () => { + vi.stubGlobal('ORCA_CLOUD_BUILD_DEFAULTS', { [key]: 'https://build.onorca.dev' }) + expect(resolve({ [envName]: 'http://127.0.0.1:8787' }, false)).toBe('http://127.0.0.1:8787') + }) + }) +}) diff --git a/src/main/cloud-build-defaults.test.ts b/src/main/cloud-build-defaults.test.ts index a0d92feccaa..4a8aecfb6f8 100644 --- a/src/main/cloud-build-defaults.test.ts +++ b/src/main/cloud-build-defaults.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { cloudDevSigningAllowed, withCloudBuildDefaults } from './cloud-build-defaults' +import { + buildServiceUrlDefault, + cloudDevSigningAllowed, + withCloudBuildDefaults +} from './cloud-build-defaults' describe('withCloudBuildDefaults', () => { afterEach(() => vi.unstubAllGlobals()) @@ -24,6 +28,20 @@ describe('withCloudBuildDefaults', () => { }) }) + it('returns a build-time service URL default, else the fallback', () => { + expect(buildServiceUrlDefault('pushGatewayUrl', 'https://fallback.example')).toBe( + 'https://fallback.example' + ) + vi.stubGlobal('ORCA_CLOUD_BUILD_DEFAULTS', { pushGatewayUrl: ' ' }) + expect(buildServiceUrlDefault('pushGatewayUrl', 'https://fallback.example')).toBe( + 'https://fallback.example' + ) + vi.stubGlobal('ORCA_CLOUD_BUILD_DEFAULTS', { pushGatewayUrl: 'https://push.example' }) + expect(buildServiceUrlDefault('pushGatewayUrl', 'https://fallback.example')).toBe( + 'https://push.example' + ) + }) + it('allows dev signing in packaged builds only when the build opts in', () => { expect(cloudDevSigningAllowed(false)).toBe(true) expect(cloudDevSigningAllowed(true)).toBe(false) diff --git a/src/main/cloud-build-defaults.ts b/src/main/cloud-build-defaults.ts index 568fddb4b53..c5ab91f97d3 100644 --- a/src/main/cloud-build-defaults.ts +++ b/src/main/cloud-build-defaults.ts @@ -1,20 +1,33 @@ // Packaged builds read no shell env, so the integration build bakes in its backend URLs // (config/cloud-build-defaults.json via electron.vite.config.ts). Run-time env still wins. -declare const ORCA_CLOUD_BUILD_DEFAULTS: { - billingApiUrl?: string - cloudSandboxUrl?: string - allowDevSigning?: boolean -} | null +type BuildServiceUrlDefaults = { + authApiUrl?: string + relayDirectorUrl?: string + pushGatewayUrl?: string + connectorUrl?: string + artifactsApiUrl?: string +} -function buildDefaults(): { +type CloudBuildDefaults = BuildServiceUrlDefaults & { billingApiUrl?: string cloudSandboxUrl?: string allowDevSigning?: boolean -} { +} + +declare const ORCA_CLOUD_BUILD_DEFAULTS: CloudBuildDefaults | null + +function buildDefaults(): CloudBuildDefaults { // oxlint-disable-next-line unicorn/no-typeof-undefined -- undeclared outside electron-vite builds; a bare read throws. return (typeof ORCA_CLOUD_BUILD_DEFAULTS === 'undefined' ? null : ORCA_CLOUD_BUILD_DEFAULTS) ?? {} } +export type BuildServiceUrlKey = keyof BuildServiceUrlDefaults + +/** Build-time service URL default, or `fallback` when the build sets none. */ +export function buildServiceUrlDefault(key: BuildServiceUrlKey, fallback: string): string { + return buildDefaults()[key]?.trim() || fallback +} + export function withCloudBuildDefaults(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const defaults = buildDefaults() return { diff --git a/src/main/connector-cloud/connector-origin.ts b/src/main/connector-cloud/connector-origin.ts index 84684643eca..382f87ca230 100644 --- a/src/main/connector-cloud/connector-origin.ts +++ b/src/main/connector-cloud/connector-origin.ts @@ -1,7 +1,9 @@ import { cleanCloudServiceOrigin } from '../../shared/cloud-service-url' +import { buildServiceUrlDefault } from '../cloud-build-defaults' export function resolveConnectorOrigin(env: NodeJS.ProcessEnv, packaged: boolean): string { return ( - cleanCloudServiceOrigin(env.ORCA_CONNECTOR_URL, !packaged) ?? 'https://connector.onorca.dev' + cleanCloudServiceOrigin(env.ORCA_CONNECTOR_URL, !packaged) ?? + buildServiceUrlDefault('connectorUrl', 'https://connector.onorca.dev') ) } diff --git a/src/main/orca-profiles/cloud-auth/profile-cloud-auth-config.ts b/src/main/orca-profiles/cloud-auth/profile-cloud-auth-config.ts index fc25268707e..3ce51719230 100644 --- a/src/main/orca-profiles/cloud-auth/profile-cloud-auth-config.ts +++ b/src/main/orca-profiles/cloud-auth/profile-cloud-auth-config.ts @@ -4,6 +4,7 @@ import { cleanCloudServiceOrigin as cleanOrigin } from '../../../shared/cloud-service-url' import { resolvePushGatewayOrigin } from '../../runtime/push/push-gateway-origin' +import { buildServiceUrlDefault } from '../../cloud-build-defaults' export type OrcaCloudAuthConfig = { apiBaseUrl: string @@ -52,7 +53,7 @@ export function getOrcaCloudAuthConfig( const apiBaseUrl = configuredApiBaseUrl ? cleanEndpointUrl(configuredApiBaseUrl) : packaged - ? PRODUCTION_API_BASE_URL + ? buildServiceUrlDefault('authApiUrl', PRODUCTION_API_BASE_URL) : null const clientId = env.ORCA_CLOUD_CLIENT_ID?.trim() || (packaged ? PRODUCTION_CLIENT_ID : undefined) if (!apiBaseUrl || !clientId) { @@ -94,7 +95,8 @@ export function getOrcaCloudAuthConfig( cleanEndpointUrl(env.ORCA_CLOUD_CONNECTOR_TOKEN_URL) ?? endpoint(apiBaseUrl, '/v1/desktop/auth/connector-token'), relayDirectorUrl: - cleanOrigin(env.ORCA_RELAY_URL, allowLoopbackHttp) ?? PRODUCTION_RELAY_DIRECTOR_URL, + cleanOrigin(env.ORCA_RELAY_URL, allowLoopbackHttp) ?? + buildServiceUrlDefault('relayDirectorUrl', PRODUCTION_RELAY_DIRECTOR_URL), clientId, scope: env.ORCA_CLOUD_AUTH_SCOPE?.trim() || DEFAULT_SCOPE } diff --git a/src/main/runtime/push/push-gateway-origin.ts b/src/main/runtime/push/push-gateway-origin.ts index f4f78f2ed38..48fab048dae 100644 --- a/src/main/runtime/push/push-gateway-origin.ts +++ b/src/main/runtime/push/push-gateway-origin.ts @@ -1,5 +1,9 @@ import { cleanCloudServiceOrigin } from '../../../shared/cloud-service-url' +import { buildServiceUrlDefault } from '../../cloud-build-defaults' export function resolvePushGatewayOrigin(env: NodeJS.ProcessEnv, packaged: boolean): string { - return cleanCloudServiceOrigin(env.ORCA_PUSH_GATEWAY_URL, !packaged) ?? 'https://push.onorca.dev' + return ( + cleanCloudServiceOrigin(env.ORCA_PUSH_GATEWAY_URL, !packaged) ?? + buildServiceUrlDefault('pushGatewayUrl', 'https://push.onorca.dev') + ) }