mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
feat(cloud): issuer lists, platform egress suffixes and a prod-required setup pages URL (DOMAIN-1 D2) (#668)
- SANDBOX_/BILLING_(STAGING_)AUTH_ISSUER accept a comma-separated issuer list; a single value behaves as before. - ORCA_PLATFORM_HOST_SUFFIXES supplies the platform egress hosts (default *.onorca.dev, unchanged allowlist). - Billing: CLOUD_SETUP_PAGES_URL unset with BILLING_ENV=prod answers 503 billing_misconfigured; dev/staging keep the fallback. No wrangler config or current value changes.
This commit is contained in:
@@ -63,10 +63,22 @@ function devKeysFor(env: Env): JWTVerifyGetKey | null {
|
||||
return devKeys.keys
|
||||
}
|
||||
|
||||
/**
|
||||
* A comma-separated issuer var as jwtVerify's `issuer` option: the token's `iss` must equal one entry.
|
||||
* Unset or empty: no issuer check (as before lists). Blank entries are ignored; a value with no
|
||||
* entries left accepts no issuer.
|
||||
*/
|
||||
export function issuerList(value: string | undefined): string[] | undefined {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
return value.split(',').map((issuer) => issuer.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
let stagingKeys: { source: string; keys: JWTVerifyGetKey } | null = null
|
||||
|
||||
/** Staging auth's keys and issuer; never honored when BILLING_ENV is 'prod'. */
|
||||
function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string } | null {
|
||||
function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string[] | undefined } | null {
|
||||
if (env.BILLING_ENV === 'prod' || !env.BILLING_STAGING_JWKS_URL || !env.BILLING_STAGING_AUTH_ISSUER) {
|
||||
return null
|
||||
}
|
||||
@@ -76,7 +88,7 @@ function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string } | n
|
||||
keys: createRemoteJWKSet(new URL(env.BILLING_STAGING_JWKS_URL))
|
||||
}
|
||||
}
|
||||
return { keys: stagingKeys.keys, issuer: env.BILLING_STAGING_AUTH_ISSUER }
|
||||
return { keys: stagingKeys.keys, issuer: issuerList(env.BILLING_STAGING_AUTH_ISSUER) }
|
||||
}
|
||||
|
||||
export function readBearer(value: string | null): string | null {
|
||||
@@ -102,7 +114,7 @@ export async function authenticateDesktop(
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
const verify = (keys: JWTVerifyGetKey, issuer: string | undefined) =>
|
||||
const verify = (keys: JWTVerifyGetKey, issuer: string | string[] | undefined) =>
|
||||
jwtVerify(token, keys, {
|
||||
audience: env.BILLING_JWT_AUDIENCE,
|
||||
algorithms: ['ES256'],
|
||||
@@ -114,7 +126,7 @@ export async function authenticateDesktop(
|
||||
try {
|
||||
// Production auth first; in dev only, then staging auth, then the operator key set.
|
||||
const attempts: Array<() => ReturnType<typeof verify>> = [
|
||||
() => verify(keysFor(env), env.BILLING_AUTH_ISSUER)
|
||||
() => verify(keysFor(env), issuerList(env.BILLING_AUTH_ISSUER))
|
||||
]
|
||||
const staging = stagingKeysFor(env)
|
||||
if (staging) {
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
// URLs of the cloud setup pages on Orca Cloud auth (auth.onorca.dev/cloud/setup/...).
|
||||
import type { Env } from './env'
|
||||
|
||||
/** Thrown when CLOUD_SETUP_PAGES_URL is unset in prod; the index answers 503 billing_misconfigured. */
|
||||
export class SetupPagesConfigError extends Error {
|
||||
constructor() {
|
||||
super('cloud_setup_pages_url_missing')
|
||||
this.name = 'SetupPagesConfigError'
|
||||
}
|
||||
}
|
||||
|
||||
function pagesOrigin(env: Env): string {
|
||||
if (!env.CLOUD_SETUP_PAGES_URL && env.BILLING_ENV === 'prod') {
|
||||
// Prod must name its pages origin; the fallback below is only a dev/staging convenience.
|
||||
throw new SetupPagesConfigError()
|
||||
}
|
||||
return (env.CLOUD_SETUP_PAGES_URL ?? 'https://auth.onorca.dev').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export type Env = {
|
||||
// BILLING_JWKS (inline set) is for local dev and tests only.
|
||||
BILLING_JWKS_URL?: string
|
||||
BILLING_JWKS?: string
|
||||
// Accepted `iss` values, comma-separated (a single issuer is a one-item list); so is the staging one.
|
||||
BILLING_AUTH_ISSUER?: string
|
||||
BILLING_JWT_AUDIENCE: string
|
||||
// 'dev' | 'prod'. Dev may also accept BILLING_DEV_JWKS (issuer 'orca-billing-dev-test', a secret
|
||||
|
||||
@@ -55,6 +55,7 @@ import { entitlementsView, offersFor, plansView } from './plans-api'
|
||||
import { reconcile } from './reconcile'
|
||||
import type { Env } from './env'
|
||||
import { MACHINE_SIZES, PlanConfigError, planCatalog, publicOffers } from './plans'
|
||||
import { SetupPagesConfigError } from './cloud-setup-urls'
|
||||
import { OverageRequestSchema, updateOverage } from './overage'
|
||||
import { LiveModeRefusedError, StripeError, createStripeClient, type StripeClient } from './stripe'
|
||||
import { UsageReportSchema, flushMeterReports, recordUsage, validateSegment } from './usage'
|
||||
@@ -566,7 +567,12 @@ export async function handleRequest(request: Request, env: Env, ctx: ExecutionCo
|
||||
if (error instanceof SyntaxError) {
|
||||
return json({ error: 'invalid_json' }, 400)
|
||||
}
|
||||
if (error instanceof LiveModeRefusedError || error instanceof PlanConfigError || error instanceof CatalogUnavailableError) {
|
||||
if (
|
||||
error instanceof LiveModeRefusedError ||
|
||||
error instanceof PlanConfigError ||
|
||||
error instanceof CatalogUnavailableError ||
|
||||
error instanceof SetupPagesConfigError
|
||||
) {
|
||||
return json({ error: 'billing_misconfigured' }, 503)
|
||||
}
|
||||
if (error instanceof MembershipUnavailableError) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { SignJWT, exportJWK, generateKeyPair } from 'jose'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { authenticateDesktop, issuerList } from '../src/auth'
|
||||
import { createHarness } from './harness'
|
||||
|
||||
const PROD = 'https://login.onorca.dev'
|
||||
const NEXT = 'https://login.example.dev'
|
||||
const NOW = Date.UTC(2026, 0, 15)
|
||||
|
||||
async function issuerHarness(issuers: string) {
|
||||
const { privateKey, publicKey } = await generateKeyPair('ES256')
|
||||
const jwk = { ...(await exportJWK(publicKey)), kid: 'iss1', alg: 'ES256', use: 'sig' }
|
||||
const h = await createHarness({ BILLING_JWKS: JSON.stringify({ keys: [jwk] }), BILLING_AUTH_ISSUER: issuers })
|
||||
const accepts = async (issuer: string): Promise<boolean> => {
|
||||
const iat = Math.floor(NOW / 1000)
|
||||
const token = await new SignJWT({ sub: 'user_iss', purpose: 'billing' })
|
||||
.setProtectedHeader({ alg: 'ES256', kid: 'iss1' })
|
||||
.setIssuer(issuer)
|
||||
.setAudience('orca-billing')
|
||||
.setIssuedAt(iat)
|
||||
.setExpirationTime(iat + 300)
|
||||
.sign(privateKey)
|
||||
const request = new Request('https://billing.test/v1/billing/entitlements', { headers: { authorization: `Bearer ${token}` } })
|
||||
return (await authenticateDesktop(h.env, request)) !== null
|
||||
}
|
||||
return { accepts }
|
||||
}
|
||||
|
||||
describe('BILLING_AUTH_ISSUER list', () => {
|
||||
// jose checks exp/iat against Date; pin it so no wall clock is involved.
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
vi.setSystemTime(NOW)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('parses a comma list, ignoring whitespace and empty entries', () => {
|
||||
expect(issuerList(undefined)).toBeUndefined()
|
||||
expect(issuerList('')).toBeUndefined()
|
||||
expect(issuerList(PROD)).toEqual([PROD])
|
||||
expect(issuerList(` ${PROD} ,, ${NEXT} , `)).toEqual([PROD, NEXT])
|
||||
})
|
||||
|
||||
it('a single issuer behaves as before', async () => {
|
||||
const h = await issuerHarness(PROD)
|
||||
expect(await h.accepts(PROD)).toBe(true)
|
||||
expect(await h.accepts(NEXT)).toBe(false)
|
||||
})
|
||||
|
||||
it('a list accepts each listed issuer and rejects others', async () => {
|
||||
const h = await issuerHarness(` ${PROD} , ,${NEXT},`)
|
||||
expect(await h.accepts(PROD)).toBe(true)
|
||||
expect(await h.accepts(NEXT)).toBe(true)
|
||||
expect(await h.accepts('https://login.evil.dev')).toBe(false)
|
||||
})
|
||||
|
||||
it('a value with no entries accepts no issuer', async () => {
|
||||
const h = await issuerHarness(' , ')
|
||||
expect(await h.accepts(PROD)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SetupPagesConfigError, setupPageUrl } from '../src/cloud-setup-urls'
|
||||
import { createHarness } from './harness'
|
||||
|
||||
const NOW = Date.UTC(2026, 0, 15)
|
||||
|
||||
describe('CLOUD_SETUP_PAGES_URL', () => {
|
||||
// The harness and jose read Date; pin it so no wall clock is involved.
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ['Date'] })
|
||||
vi.setSystemTime(NOW)
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('falls back to the current auth origin outside prod when unset', async () => {
|
||||
for (const BILLING_ENV of [undefined, 'dev', 'staging']) {
|
||||
const { env } = await createHarness({ BILLING_ENV })
|
||||
expect(setupPageUrl(env, '/start', { session: 's1' })).toBe('https://auth.onorca.dev/cloud/setup/start?session=s1')
|
||||
}
|
||||
})
|
||||
|
||||
it('uses the configured origin in every environment', async () => {
|
||||
for (const BILLING_ENV of ['dev', 'prod']) {
|
||||
const { env } = await createHarness({ BILLING_ENV, CLOUD_SETUP_PAGES_URL: 'https://pages.test/' })
|
||||
expect(setupPageUrl(env, '/plan', { session: 's1' })).toBe('https://pages.test/cloud/setup/plan?session=s1')
|
||||
}
|
||||
})
|
||||
|
||||
it('is a configuration error in prod when unset, answered as billing_misconfigured', async () => {
|
||||
const h = await createHarness({ BILLING_ENV: 'prod' })
|
||||
expect(() => setupPageUrl(h.env, '/start', { session: 's1' })).toThrow(SetupPagesConfigError)
|
||||
const token = await h.desktopToken({ sub: 'u1', email: 'neil@acme.dev' })
|
||||
const created = await h.call('POST', '/v1/cloud-setup/sessions', {
|
||||
body: { repo: 'https://github.com/acme/web', needs: ['plan'], device: 'MacBook Pro' },
|
||||
headers: { authorization: `Bearer ${token}` }
|
||||
})
|
||||
expect(created).toEqual({ status: 503, body: { error: 'billing_misconfigured' } })
|
||||
})
|
||||
})
|
||||
@@ -97,10 +97,22 @@ export function accountIdFor(claims: { sub: string; org?: string }): string {
|
||||
return claims.org ? `org:${claims.org}` : `user:${claims.sub}`
|
||||
}
|
||||
|
||||
/**
|
||||
* A comma-separated issuer var as jwtVerify's `issuer` option: the token's `iss` must equal one entry.
|
||||
* Unset or empty: no issuer check (as before lists). Blank entries are ignored; a value with no
|
||||
* entries left accepts no issuer.
|
||||
*/
|
||||
export function issuerList(value: string | undefined): string[] | undefined {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
return value.split(',').map((issuer) => issuer.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
let stagingKeys: { source: string; keys: JWTVerifyGetKey } | null = null
|
||||
|
||||
/** Staging auth's keys and issuer; never honored in prod. Same accounts as billing dev (it trusts staging too). */
|
||||
function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string } | null {
|
||||
function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string[] | undefined } | null {
|
||||
const source = env.SANDBOX_STAGING_JWKS_URL ?? (env.SANDBOX_STAGING_JWKS ? `inline:${env.SANDBOX_STAGING_JWKS}` : null)
|
||||
if (env.SANDBOX_ENV === 'prod' || !source || !env.SANDBOX_STAGING_AUTH_ISSUER) {
|
||||
return null
|
||||
@@ -113,7 +125,7 @@ function stagingKeysFor(env: Env): { keys: JWTVerifyGetKey; issuer: string } | n
|
||||
: createLocalJWKSet(JSON.parse(env.SANDBOX_STAGING_JWKS ?? '{"keys":[]}') as JSONWebKeySet)
|
||||
}
|
||||
}
|
||||
return { keys: stagingKeys.keys, issuer: env.SANDBOX_STAGING_AUTH_ISSUER }
|
||||
return { keys: stagingKeys.keys, issuer: issuerList(env.SANDBOX_STAGING_AUTH_ISSUER) }
|
||||
}
|
||||
|
||||
/** Dev-only operator keys (issuer DEV_TEST_ISSUER) so live tests run before Orca auth mints sandbox tokens. */
|
||||
@@ -124,7 +136,7 @@ async function verify(
|
||||
token: string,
|
||||
keys: JWTVerifyGetKey,
|
||||
env: Env,
|
||||
issuer: string | undefined,
|
||||
issuer: string | string[] | undefined,
|
||||
now: number
|
||||
): Promise<SandboxCaller | null> {
|
||||
try {
|
||||
@@ -163,7 +175,7 @@ export async function authenticateDesktop(
|
||||
}
|
||||
const staging = stagingKeysFor(env)
|
||||
const caller =
|
||||
(await verify(token, keysFor(env), env, env.SANDBOX_AUTH_ISSUER, now)) ??
|
||||
(await verify(token, keysFor(env), env, issuerList(env.SANDBOX_AUTH_ISSUER), now)) ??
|
||||
(staging ? await verify(token, staging.keys, env, staging.issuer, now) : null)
|
||||
if (caller || !env.SANDBOX_DEV_JWKS || env.SANDBOX_ENV === 'prod') {
|
||||
if (!caller || (await tokenPredatesRemoval(env.SANDBOX_DB, caller))) {
|
||||
|
||||
@@ -46,6 +46,7 @@ export type Env = {
|
||||
// SANDBOX_JWKS (inline set) is for local dev and tests only.
|
||||
SANDBOX_JWKS_URL?: string
|
||||
SANDBOX_JWKS?: string
|
||||
/** Accepted `iss` values, comma-separated (a single issuer is a one-item list). */
|
||||
SANDBOX_AUTH_ISSUER?: string
|
||||
SANDBOX_JWT_AUDIENCE: string
|
||||
/** Dev-only operator key set (issuer `orca-sandbox-dev-test`); ignored when SANDBOX_ENV=prod. */
|
||||
@@ -194,6 +195,8 @@ export type Env = {
|
||||
PUBLIC_API_URL?: string
|
||||
/** Comma list of extra egress hosts every VM may reach (dev: this Worker's workers.dev host). */
|
||||
EGRESS_EXTRA_HOSTS?: string
|
||||
/** Comma list of Orca platform egress hosts every VM may reach (default `*.onorca.dev`). */
|
||||
ORCA_PLATFORM_HOST_SUFFIXES?: string
|
||||
/** GitHub App webhook secret (secret): verifies X-Hub-Signature-256 on /v1/github/webhook. */
|
||||
GITHUB_WEBHOOK_SECRET?: string
|
||||
/**
|
||||
|
||||
@@ -200,6 +200,7 @@ import {
|
||||
userAuthorizeUrl
|
||||
} from './github-user-tokens'
|
||||
import { MEMBER_GIT_WINDOW_MS, memberGitRoutes, memberSentWithin, recordMemberSend } from './member-git'
|
||||
import { egressAllowlistFor } from './network-policy'
|
||||
import { getWorkspacePrefs, putWorkspacePrefs } from './workspace-prefs'
|
||||
import { getOrgSlug, putOrgSlug } from './org-slugs'
|
||||
import {
|
||||
@@ -937,6 +938,7 @@ export async function lifecycleDeps(env: Env, deps: Deps): Promise<LifecycleDeps
|
||||
config: {
|
||||
pool: poolConfigFromEnv(env),
|
||||
shareLinkSecret: shareSecret(env),
|
||||
egressAllowlist: egressAllowlistFor(env.ORCA_PLATFORM_HOST_SUFFIXES),
|
||||
extraEgressHosts: extraEgressHostsFor(env),
|
||||
guardrails: guardrailsFromEnv(env),
|
||||
snapshotExpirationMs:
|
||||
|
||||
@@ -149,6 +149,8 @@ export type LifecycleDeps = {
|
||||
snapshotExpirationMs?: number
|
||||
/** Warm pool (pool.ts); absent or size 0 means every create boots its own VM. */
|
||||
pool?: PoolConfig
|
||||
/** Base egress allowlist (egressAllowlistFor ORCA_PLATFORM_HOST_SUFFIXES); default DEFAULT_EGRESS_ALLOWLIST. */
|
||||
egressAllowlist?: readonly string[]
|
||||
/** Hosts allowed on top of the default egress allowlist (e.g. this control plane for the in-VM CLI). */
|
||||
extraEgressHosts?: readonly string[]
|
||||
/** SHARE_LINK_SECRET: each session's share-proxy key derives from it; unset = no share links. */
|
||||
@@ -314,7 +316,7 @@ export function egressPolicy(deps: LifecycleDeps, row: SandboxRow, gitToken?: st
|
||||
: []
|
||||
// Custom model-provider hosts (ANTHROPIC_BASE_URL / OPENAI_BASE_URL secrets) recorded at boot,
|
||||
// plus operator-configured extra hosts.
|
||||
const allowlist = [...new Set([...DEFAULT_EGRESS_ALLOWLIST, ...(deps.config.extraEgressHosts ?? []), ...egressAllowHosts(row)])]
|
||||
const allowlist = [...new Set([...(deps.config.egressAllowlist ?? DEFAULT_EGRESS_ALLOWLIST), ...(deps.config.extraEgressHosts ?? []), ...egressAllowHosts(row)])]
|
||||
return compileNetworkPolicy({ mode: 'allowlist', allowlist, credentials: [...git, ...credentials] })
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,11 @@ export const EDITOR_REMOTE_SERVER_HOSTS: readonly string[] = [
|
||||
'openvsxorg.blob.core.windows.net'
|
||||
]
|
||||
|
||||
/** Registries, git hosting and agent model/sign-in hosts a coding VM needs. */
|
||||
export const DEFAULT_EGRESS_ALLOWLIST: readonly string[] = [
|
||||
/** Orca's own hosts when ORCA_PLATFORM_HOST_SUFFIXES is unset. */
|
||||
export const DEFAULT_PLATFORM_HOST_SUFFIXES: readonly string[] = ['*.onorca.dev']
|
||||
|
||||
/** Registries, git hosting and agent model/sign-in hosts a coding VM needs (platform hosts excluded). */
|
||||
const BASE_EGRESS_HOSTS: readonly string[] = [
|
||||
'registry.npmjs.org',
|
||||
'registry.yarnpkg.com',
|
||||
'pypi.org',
|
||||
@@ -94,11 +97,21 @@ export const DEFAULT_EGRESS_ALLOWLIST: readonly string[] = [
|
||||
'models.dev',
|
||||
// GitHub Copilot CLI: Copilot API (per plan: api.individual., api.business., ...).
|
||||
'api.githubcopilot.com',
|
||||
'*.githubcopilot.com',
|
||||
'*.onorca.dev',
|
||||
...EDITOR_REMOTE_SERVER_HOSTS
|
||||
'*.githubcopilot.com'
|
||||
]
|
||||
|
||||
/**
|
||||
* The default allowlist with Orca's platform hosts from ORCA_PLATFORM_HOST_SUFFIXES (comma list, e.g.
|
||||
* `*.onorca.dev`). Unset, blank or only commas: DEFAULT_PLATFORM_HOST_SUFFIXES.
|
||||
*/
|
||||
export function egressAllowlistFor(platformHostSuffixes?: string): readonly string[] {
|
||||
const configured = (platformHostSuffixes ?? '').split(',').map((host) => host.trim()).filter(Boolean)
|
||||
const platform = configured.length > 0 ? configured : DEFAULT_PLATFORM_HOST_SUFFIXES
|
||||
return [...new Set([...BASE_EGRESS_HOSTS, ...platform, ...EDITOR_REMOTE_SERVER_HOSTS])]
|
||||
}
|
||||
|
||||
export const DEFAULT_EGRESS_ALLOWLIST: readonly string[] = egressAllowlistFor()
|
||||
|
||||
export type CredentialRoute = {
|
||||
host: string
|
||||
/** Header the VM sends with a placeholder value, matched exactly (one claim rule per value). */
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { SignJWT, exportJWK, generateKeyPair } from 'jose'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { authenticateDesktop, issuerList } from '../src/auth'
|
||||
import { createHarness } from './harness'
|
||||
|
||||
const PROD = 'https://login.onorca.dev'
|
||||
const NEXT = 'https://login.example.dev'
|
||||
|
||||
async function issuerHarness(issuers: string) {
|
||||
const { privateKey, publicKey } = await generateKeyPair('ES256')
|
||||
const jwk = { ...(await exportJWK(publicKey)), kid: 'iss1', alg: 'ES256', use: 'sig' }
|
||||
const h = await createHarness({ SANDBOX_JWKS: JSON.stringify({ keys: [jwk] }), SANDBOX_AUTH_ISSUER: issuers })
|
||||
const accepts = async (issuer: string): Promise<boolean> => {
|
||||
const iat = Math.floor(h.now() / 1000)
|
||||
const token = await new SignJWT({ sub: 'user_iss', purpose: 'sandbox' })
|
||||
.setProtectedHeader({ alg: 'ES256', kid: 'iss1' })
|
||||
.setIssuer(issuer)
|
||||
.setAudience('orca-sandbox')
|
||||
.setIssuedAt(iat)
|
||||
.setExpirationTime(iat + 300)
|
||||
.sign(privateKey)
|
||||
const request = new Request('https://sandbox.test/v1/config', { headers: { authorization: `Bearer ${token}` } })
|
||||
return (await authenticateDesktop(h.env, request, h.now())) !== null
|
||||
}
|
||||
return { accepts }
|
||||
}
|
||||
|
||||
describe('SANDBOX_AUTH_ISSUER list', () => {
|
||||
it('parses a comma list, ignoring whitespace and empty entries', () => {
|
||||
expect(issuerList(undefined)).toBeUndefined()
|
||||
expect(issuerList('')).toBeUndefined()
|
||||
expect(issuerList(PROD)).toEqual([PROD])
|
||||
expect(issuerList(` ${PROD} ,, ${NEXT} , `)).toEqual([PROD, NEXT])
|
||||
})
|
||||
|
||||
it('a single issuer behaves as before', async () => {
|
||||
const h = await issuerHarness(PROD)
|
||||
expect(await h.accepts(PROD)).toBe(true)
|
||||
expect(await h.accepts(NEXT)).toBe(false)
|
||||
})
|
||||
|
||||
it('a list accepts each listed issuer and rejects others', async () => {
|
||||
const h = await issuerHarness(` ${PROD} , ,${NEXT},`)
|
||||
expect(await h.accepts(PROD)).toBe(true)
|
||||
expect(await h.accepts(NEXT)).toBe(true)
|
||||
expect(await h.accepts('https://login.evil.dev')).toBe(false)
|
||||
})
|
||||
|
||||
it('a value with no entries accepts no issuer', async () => {
|
||||
const h = await issuerHarness(' , ')
|
||||
expect(await h.accepts(PROD)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { EDITOR_REMOTE_SERVER_HOSTS, compileNetworkPolicy, gitBasicAuthorization } from '../src/network-policy'
|
||||
import {
|
||||
DEFAULT_EGRESS_ALLOWLIST,
|
||||
EDITOR_REMOTE_SERVER_HOSTS,
|
||||
compileNetworkPolicy,
|
||||
egressAllowlistFor,
|
||||
gitBasicAuthorization
|
||||
} from '../src/network-policy'
|
||||
|
||||
const deny = { response: { statusCode: 403, body: 'orca-egress-denied', contentType: 'text/plain' } }
|
||||
|
||||
@@ -78,3 +84,26 @@ describe('compileNetworkPolicy', () => {
|
||||
expect(allow['*']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ORCA_PLATFORM_HOST_SUFFIXES', () => {
|
||||
it('unset (or blank) keeps the default allowlist exactly as before', () => {
|
||||
const editorAt = DEFAULT_EGRESS_ALLOWLIST.length - EDITOR_REMOTE_SERVER_HOSTS.length
|
||||
expect(DEFAULT_EGRESS_ALLOWLIST).toHaveLength(56)
|
||||
expect(DEFAULT_EGRESS_ALLOWLIST.slice(editorAt - 2, editorAt)).toEqual(['*.githubcopilot.com', '*.onorca.dev'])
|
||||
expect(DEFAULT_EGRESS_ALLOWLIST.slice(editorAt)).toEqual(EDITOR_REMOTE_SERVER_HOSTS)
|
||||
for (const unset of [undefined, '', ' , ']) {
|
||||
expect(egressAllowlistFor(unset)).toEqual(DEFAULT_EGRESS_ALLOWLIST)
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces the platform hosts with the configured suffixes, once each', () => {
|
||||
const allowlist = egressAllowlistFor(' *.onorca.dev , ,*.example.dev,')
|
||||
expect(allowlist.filter((host) => host === '*.onorca.dev')).toHaveLength(1)
|
||||
expect(allowlist).toContain('*.example.dev')
|
||||
expect(allowlist).toHaveLength(DEFAULT_EGRESS_ALLOWLIST.length + 1)
|
||||
expect(egressAllowlistFor('*.example.dev')).not.toContain('*.onorca.dev')
|
||||
const { allow } = compileNetworkPolicy({ mode: 'allowlist', allowlist })
|
||||
expect(allow['*.onorca.dev']).toEqual([])
|
||||
expect(allow['*.example.dev']).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user