import { readSharedDesktopAuthSession } from '../src/auth/shared-session' import { authHeaderValue, resolveCliAuth, type CliAuth } from './auth' import { resolveDefaultUploadOrigin } from './commands/upload' import { rnxExit } from './run-rnx' type BillingSubscriptionSnapshot = { plan?: 'free' | 'personal' | 'team' entitlements?: { desktopRecording?: boolean } } async function billingOriginFor(auth: CliAuth, originOverride?: string): Promise { // an explicit override (e.g. --preview-origin https://contrast.localhost:3000 from // `maestro test --preview`) wins over whatever origin the shared auth session // happens to remember. this keeps local preview-upload flows pointed // at the local billing endpoint even when the desktop session token // was last minted against contrast. if (originOverride) return originOverride.replace(/\/$/, '') if (auth.kind === 'session' && auth.origin) { return auth.origin.replace(/\/$/, '') } const shared = readSharedDesktopAuthSession() if (shared?.origin) { return shared.origin.replace(/\/$/, '') } // api-key auth (RNX_API_KEY) carries no origin. // resolve it the same way the upload itself does — probe a local Contrast // stack on :3000, else fall back to prod. this keeps the entitlement // check pointed at the *same* origin the recording will upload to (the // engine resolves loopback sims to the local :3000 stack); checking prod // billing with a key minted against the local stack would 401 here even // though the upload would have succeeded. return (await resolveDefaultUploadOrigin()).replace(/\/$/, '') } export interface RecordingEntitlementOptions { /** override the origin used to check billing. when the CLI is running * against a local Contrast stack (e.g. `maestro test --preview --preview-origin * https://contrast.localhost:3000`), the recording entitlement needs to hit the * same origin that'll receive the upload — not the remote contrast * origin stored in the shared desktop session. */ originOverride?: string /** allow GitHub installation/action auth to record for pr preview uploads. * this is intentionally opt-in so local `rnx record` stays tied to * the user's paid/trial desktop entitlement. */ allowGitHubAuth?: boolean } export async function ensureCliRecordingEntitlement( commandLabel: string, opts: RecordingEntitlementOptions = {}, ): Promise { const auth = resolveCliAuth() if (!auth) { process.stderr.write( `\n rnx ${commandLabel} needs auth before it can check recording access.\n\n` + ` pick one:\n` + ` • run \`rnx login\`\n` + ` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`, ) rnxExit(1) } if (auth.kind === 'github' && opts.allowGitHubAuth) return const billingOrigin = await billingOriginFor(auth, opts.originOverride) let res: Response try { res = await fetch(`${billingOrigin}/api/sootsim/billing/subscription`, { headers: { authorization: authHeaderValue(auth) }, }) } catch (error) { process.stderr.write( `\n rnx ${commandLabel} couldn't verify recording access.\n` + ` ${error instanceof Error ? error.message : String(error)}\n\n`, ) rnxExit(1) } if (res.status === 401) { process.stderr.write( `\n rnx ${commandLabel} couldn't verify recording access with the current auth against ${billingOrigin}.\n` + ` sign in again for that origin, or provide a valid RNX_API_KEY.\n\n`, ) rnxExit(1) } if (!res.ok) { const text = await res.text().catch(() => '') process.stderr.write( `\n rnx ${commandLabel} couldn't verify recording access (${res.status}).\n` + ` ${text || 'unexpected billing response'}\n\n`, ) rnxExit(1) } const data = (await res.json()) as BillingSubscriptionSnapshot if (data.entitlements?.desktopRecording === true) return const plan = data.plan ?? 'free' process.stderr.write( `\n rnx ${commandLabel} recording requires Personal, Team, or an active trial.\n` + ` current plan: ${plan}.\n` + ` free keeps basic 3d mode; the trial unlocks the full Personal surface.\n\n`, ) rnxExit(1) }