mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
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.
This commit is contained in:
@@ -1,13 +1,74 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
type ServiceUrlKey =
|
||||
| 'authApiUrl'
|
||||
| 'relayDirectorUrl'
|
||||
| 'pushGatewayUrl'
|
||||
| 'connectorUrl'
|
||||
| 'artifactsApiUrl'
|
||||
|
||||
type BuildDefaultsFile = Partial<Record<ServiceUrlKey, string>> & {
|
||||
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<ServiceUrlKey, string> = {
|
||||
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<Record<ServiceUrlKey, string>> = {}
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user