mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
Billing Worker:
- trials + trial_claims (migration 0005): 2 sandbox hours per new team, no card, no Stripe.
- POST /v1/billing/trial grants it (idempotent, any member); grantTrial/trialOffer exported for
the setup session. offers.trial on GET entitlements only when this caller could still claim it.
- Abuse limits: once per team, Orca user, normalized verified email, GitHub user (token claim)
and GitHub owner (POST /v1/internal/trial/github from the binding; a reused identity revokes),
disposable-domain blocklist, 3 grants per IP per day. Keys stored as SHA-256 hashes.
- Entitlement trial {includedSeconds, usedSeconds, remainingSeconds, exhausted}; metered from
usage_events, concurrency 1, never overage; trial_used_up (suspendRunning) / trial_unavailable.
- Any subscription ends the trial; unused trial hours lapse.
- e2e: trial path (grant, no Stripe customer, concurrency, metering to used up, once per user,
plan ends it) with cleanup of the run's abuse keys.
Desktop: trial_used_up / trial_unavailable classify as 'plan'; copy in billing, phone and CLI;
"Trial" badge, TrialPlanSummary in Plan & billing and the plans page strip, account menu row
"Trial · {{count}} sandbox hours left" (Choose plan for owners/admins); billingClaimTrial IPC
and claimOrcaCloudTrial() for the composer.
104 lines
4.4 KiB
TypeScript
104 lines
4.4 KiB
TypeScript
import { SignJWT, exportJWK, generateKeyPair } from 'jose'
|
|
import type { Env } from '../src/env'
|
|
import { handleRequest } from '../src/index'
|
|
import { createStripeClient } from '../src/stripe'
|
|
import type { OrgMemberCheck } from '../src/team-plans'
|
|
import { createFakeD1 } from './d1-fake'
|
|
import { createFakeStripe, signStripePayload } from './stripe-fake'
|
|
import type { TrialPort } from '../src/cloud-setup-trial'
|
|
|
|
export const WEBHOOK_SECRET = 'whsec_test_local_fixture_secret'
|
|
export const CONTROL_TOKEN = 'cp_test_0123456789abcdef0123456789abcdef'
|
|
|
|
export async function createHarness(overrides: Partial<Env> = {}) {
|
|
const { privateKey, publicKey } = await generateKeyPair('ES256')
|
|
const jwk = { ...(await exportJWK(publicKey)), kid: `k${Math.random()}`, alg: 'ES256', use: 'sig' }
|
|
const { db, raw } = createFakeD1()
|
|
const stripe = createFakeStripe()
|
|
const env: Env = {
|
|
BILLING_DB: db,
|
|
STRIPE_SECRET_KEY: 'rk_test_fixture',
|
|
STRIPE_WEBHOOK_SECRET: WEBHOOK_SECRET,
|
|
STRIPE_API_BASE: 'https://stripe.test',
|
|
STRIPE_PRICE_TEAM_MONTHLY: 'price_team_month',
|
|
STRIPE_PRICE_TEAM_ANNUAL: 'price_team_year',
|
|
STRIPE_PRICE_OVERAGE: 'price_overage',
|
|
STRIPE_METER_EVENT_NAME: 'orca_sandbox_seconds',
|
|
BILLING_RETURN_URL: 'https://onorca.dev/billing/return',
|
|
BILLING_JWKS: JSON.stringify({ keys: [jwk] }),
|
|
BILLING_JWT_AUDIENCE: 'orca-billing',
|
|
BILLING_AUTH_ISSUER: 'https://login.onorca.dev',
|
|
CONTROL_PLANE_TOKEN: CONTROL_TOKEN,
|
|
...overrides
|
|
}
|
|
let now = Date.now()
|
|
// Live org member counts from auth; null = not configured (the default).
|
|
let membership: (orgId: string) => Promise<number | null> = async () => null
|
|
// Auth's live org-member check (cloud start B2); null = auth unavailable (the default).
|
|
let orgMember: OrgMemberCheck = async () => null
|
|
let trial: TrialPort | undefined
|
|
let outboundFetch: typeof fetch | undefined
|
|
const waits: Promise<unknown>[] = []
|
|
const ctx = { waitUntil: (promise: Promise<unknown>) => waits.push(promise), passThroughOnException() {} } as unknown as ExecutionContext
|
|
|
|
async function call(method: string, path: string, init: { body?: unknown; rawBody?: string; headers?: Record<string, string> } = {}) {
|
|
const request = new Request(`https://billing.test${path}`, {
|
|
method,
|
|
headers: { 'content-type': 'application/json', ...init.headers },
|
|
body: init.rawBody ?? (init.body === undefined ? undefined : JSON.stringify(init.body))
|
|
})
|
|
const response = await handleRequest(request, env, ctx, {
|
|
stripe: createStripeClient(env, stripe.fetchImpl),
|
|
now: () => now,
|
|
membership: (orgId) => membership(orgId),
|
|
orgMember: (orgId, billingToken) => orgMember(orgId, billingToken),
|
|
...(trial ? { trial } : {}),
|
|
...(outboundFetch ? { fetch: outboundFetch } : {})
|
|
})
|
|
await Promise.all(waits.splice(0))
|
|
return { status: response.status, body: (await response.json()) as Record<string, any> }
|
|
}
|
|
|
|
return {
|
|
env,
|
|
raw,
|
|
stripe,
|
|
call,
|
|
setNow(value: number) {
|
|
now = value
|
|
},
|
|
setMembership(source: (orgId: string) => Promise<number | null>) {
|
|
membership = source
|
|
},
|
|
setOrgMember(check: OrgMemberCheck) {
|
|
orgMember = check
|
|
},
|
|
setTrial(port: TrialPort) {
|
|
trial = port
|
|
},
|
|
setFetch(impl: typeof fetch) {
|
|
outboundFetch = impl
|
|
},
|
|
async desktopToken(claims: { sub: string; org?: string; orgRole?: string; email?: string; orgMembers?: number; emailVerified?: boolean; githubUserId?: string }, options: { audience?: string; purpose?: string; ageSeconds?: number } = {}) {
|
|
const iat = Math.floor(now / 1000) - (options.ageSeconds ?? 0)
|
|
return new SignJWT({ ...claims, purpose: options.purpose ?? 'billing' })
|
|
.setProtectedHeader({ alg: 'ES256', kid: jwk.kid })
|
|
.setIssuer('https://login.onorca.dev')
|
|
.setAudience(options.audience ?? 'orca-billing')
|
|
.setIssuedAt(iat)
|
|
.setExpirationTime(iat + 900)
|
|
.sign(privateKey)
|
|
},
|
|
async webhook(event: unknown, secret = WEBHOOK_SECRET) {
|
|
const rawBody = JSON.stringify(event)
|
|
return call('POST', '/v1/stripe/webhook', {
|
|
rawBody,
|
|
headers: { 'stripe-signature': signStripePayload(rawBody, secret, Math.floor(now / 1000)) }
|
|
})
|
|
},
|
|
controlPlane(path: string, body: unknown, token = CONTROL_TOKEN) {
|
|
return call('POST', path, { body, headers: { authorization: `Bearer ${token}` } })
|
|
}
|
|
}
|
|
}
|