Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/deploy-den-control-plane.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ jobs:
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
run: |
missing=0
for key in RENDER_API_KEY RENDER_DEN_CONTROL_PLANE_SERVICE_ID RENDER_OWNER_ID DEN_DATABASE_URL DEN_BETTER_AUTH_SECRET; do
Expand All @@ -37,6 +41,18 @@ jobs:
missing=1
fi
done

feature_enabled="${DEN_POLAR_FEATURE_GATE_ENABLED:-false}"
feature_enabled="$(echo "$feature_enabled" | tr '[:upper:]' '[:lower:]')"
if [ "$feature_enabled" = "true" ]; then
for key in POLAR_ACCESS_TOKEN POLAR_PRODUCT_ID POLAR_BENEFIT_ID; do
if [ -z "${!key}" ]; then
echo "::error::Missing required paywall secret: $key"
missing=1
fi
done
fi

if [ "$missing" -ne 0 ]; then
exit 1
fi
Expand All @@ -49,6 +65,11 @@ jobs:
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
DEN_RENDER_WORKER_OPENWORK_VERSION: ${{ vars.DEN_RENDER_WORKER_OPENWORK_VERSION }}
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
DEN_POLAR_API_BASE: ${{ vars.DEN_POLAR_API_BASE }}
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
run: |
python3 <<'PY'
import json
Expand All @@ -61,6 +82,16 @@ jobs:
service_id = os.environ["RENDER_DEN_CONTROL_PLANE_SERVICE_ID"]
owner_id = os.environ["RENDER_OWNER_ID"]
openwork_version = os.environ.get("DEN_RENDER_WORKER_OPENWORK_VERSION") or "0.11.113"
paywall_enabled = (os.environ.get("DEN_POLAR_FEATURE_GATE_ENABLED") or "false").lower() == "true"
polar_api_base = os.environ.get("DEN_POLAR_API_BASE") or "https://api.polar.sh"
polar_access_token = os.environ.get("POLAR_ACCESS_TOKEN") or ""
polar_product_id = os.environ.get("POLAR_PRODUCT_ID") or ""
polar_benefit_id = os.environ.get("POLAR_BENEFIT_ID") or ""

if paywall_enabled and (not polar_access_token or not polar_product_id or not polar_benefit_id):
raise RuntimeError(
"DEN_POLAR_FEATURE_GATE_ENABLED=true requires POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, and POLAR_BENEFIT_ID"
)

headers = {
"Authorization": f"Bearer {api_key}",
Expand Down Expand Up @@ -105,6 +136,13 @@ jobs:
{"key": "RENDER_PROVISION_TIMEOUT_MS", "value": "900000"},
{"key": "RENDER_HEALTHCHECK_TIMEOUT_MS", "value": "180000"},
{"key": "RENDER_POLL_INTERVAL_MS", "value": "5000"},
{"key": "POLAR_FEATURE_GATE_ENABLED", "value": "true" if paywall_enabled else "false"},
{"key": "POLAR_API_BASE", "value": polar_api_base},
{"key": "POLAR_ACCESS_TOKEN", "value": polar_access_token},
{"key": "POLAR_PRODUCT_ID", "value": polar_product_id},
{"key": "POLAR_BENEFIT_ID", "value": polar_benefit_id},
{"key": "POLAR_SUCCESS_URL", "value": service_url},
{"key": "POLAR_RETURN_URL", "value": service_url},
]

request("PUT", f"/services/{service_id}/env-vars", env_vars)
Expand Down
7 changes: 7 additions & 0 deletions services/den-control-plane/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ RENDER_WORKER_NAME_PREFIX=den-worker
RENDER_PROVISION_TIMEOUT_MS=900000
RENDER_HEALTHCHECK_TIMEOUT_MS=180000
RENDER_POLL_INTERVAL_MS=5000
POLAR_FEATURE_GATE_ENABLED=false
POLAR_API_BASE=https://api.polar.sh
POLAR_ACCESS_TOKEN=
POLAR_PRODUCT_ID=
POLAR_BENEFIT_ID=
POLAR_SUCCESS_URL=http://localhost:8788
POLAR_RETURN_URL=http://localhost:8788
74 changes: 74 additions & 0 deletions services/den-control-plane/AGENT_FOCUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Agent Focus: Den Control Plane + Polar Gate

This guide explains how agents should operate, test, and troubleshoot the Den control plane.

## What this service does

- Handles auth (`/api/auth/*`) and session lookup (`/v1/me`).
- Creates workers (`/v1/workers`) and provisions cloud workers on Render.
- Optionally enforces a Polar paywall for cloud worker creation.

## Core flows to test

### 1) Auth flow

1. `POST /api/auth/sign-up/email`
2. `GET /v1/me` using cookie session
3. `GET /v1/me` using Bearer token from sign-up response

Expected: all succeed with `200`.

### 2) Cloud worker flow (no paywall)

Set `POLAR_FEATURE_GATE_ENABLED=false`.

1. `POST /v1/workers` with `destination="cloud"`
2. Confirm `instance.provider="render"`
3. Poll `instance.url + "/health"`

Expected: worker creation `201`, worker health `200`.

### 3) Cloud worker flow (paywall enabled)

Set all Polar env vars and `POLAR_FEATURE_GATE_ENABLED=true`.

For a user without entitlement:

1. `POST /v1/workers` with `destination="cloud"`

Expected:

- `402 payment_required`
- response contains `polar.checkoutUrl`

For an entitled user (has the required Polar benefit):

1. `POST /v1/workers` with `destination="cloud"`

Expected: worker creation `201` with Render-backed instance.

## Required env vars (summary)

- Base: `DATABASE_URL`, `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`
- Render: `PROVISIONER_MODE=render`, `RENDER_API_KEY`, `RENDER_OWNER_ID`, and `RENDER_WORKER_*`
- Polar gate:
- `POLAR_FEATURE_GATE_ENABLED`
- `POLAR_ACCESS_TOKEN`
- `POLAR_PRODUCT_ID`
- `POLAR_BENEFIT_ID`
- `POLAR_SUCCESS_URL`
- `POLAR_RETURN_URL`

## Deployment behavior

`dev` is the production branch for this service. Workflow:

- `.github/workflows/deploy-den-control-plane.yml`

It updates Render env vars and triggers a deploy for the configured service ID.

## Common failure modes

- `provisioning_failed`: Render deploy failed or health check timed out.
- `payment_required`: Polar gate is enabled and user does not have the required benefit.
- startup error: paywall enabled but missing Polar env vars.
8 changes: 8 additions & 0 deletions services/den-control-plane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ pnpm dev
- `RENDER_PROVISION_TIMEOUT_MS` max time to wait for deploy to become live
- `RENDER_HEALTHCHECK_TIMEOUT_MS` max time to wait for worker health checks
- `RENDER_POLL_INTERVAL_MS` polling interval for deploy + health checks
- `POLAR_FEATURE_GATE_ENABLED` enable cloud-worker paywall (`true` or `false`)
- `POLAR_API_BASE` Polar API base URL (default `https://api.polar.sh`)
- `POLAR_ACCESS_TOKEN` Polar organization access token (required when paywall enabled)
- `POLAR_PRODUCT_ID` Polar product ID used for checkout sessions (required when paywall enabled)
- `POLAR_BENEFIT_ID` Polar benefit ID required to unlock cloud workers (required when paywall enabled)
- `POLAR_SUCCESS_URL` redirect URL after successful checkout (required when paywall enabled)
- `POLAR_RETURN_URL` return URL shown in checkout (required when paywall enabled)

## Auth setup (Better Auth)

Expand All @@ -54,6 +61,7 @@ pnpm db:migrate
- `GET /` demo web app (sign-up + auth + worker launch)
- `GET /v1/me`
- `POST /v1/workers`
- Returns `402 payment_required` with Polar checkout URL when paywall is enabled and entitlement is missing.
- `GET /v1/workers/:id`
- `POST /v1/workers/:id/tokens`

Expand Down
135 changes: 135 additions & 0 deletions services/den-control-plane/src/billing/polar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { env } from "../env.js"

type PolarCustomerState = {
granted_benefits?: Array<{
benefit_id?: string
}>
}

type PolarCheckoutSession = {
url?: string
}

export type CloudWorkerAccess =
| {
allowed: true
}
| {
allowed: false
checkoutUrl: string
}

type CloudAccessInput = {
userId: string
email: string
name: string
}

function sanitizeApiBase(value: string) {
return value.replace(/\/+$/, "")
}

async function polarFetch(path: string, init: RequestInit = {}) {
const headers = new Headers(init.headers)
headers.set("Authorization", `Bearer ${env.polar.accessToken}`)
headers.set("Accept", "application/json")
if (init.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json")
}

return fetch(`${sanitizeApiBase(env.polar.apiBase)}${path}`, {
...init,
headers,
})
}

function assertPaywallConfig() {
if (!env.polar.accessToken) {
throw new Error("POLAR_ACCESS_TOKEN is required when POLAR_FEATURE_GATE_ENABLED=true")
}
if (!env.polar.productId) {
throw new Error("POLAR_PRODUCT_ID is required when POLAR_FEATURE_GATE_ENABLED=true")
}
if (!env.polar.benefitId) {
throw new Error("POLAR_BENEFIT_ID is required when POLAR_FEATURE_GATE_ENABLED=true")
}
if (!env.polar.successUrl) {
throw new Error("POLAR_SUCCESS_URL is required when POLAR_FEATURE_GATE_ENABLED=true")
}
if (!env.polar.returnUrl) {
throw new Error("POLAR_RETURN_URL is required when POLAR_FEATURE_GATE_ENABLED=true")
}
}

async function getCustomerState(externalCustomerId: string): Promise<PolarCustomerState | null> {
const encodedExternalId = encodeURIComponent(externalCustomerId)
const response = await polarFetch(`/v1/customers/external/${encodedExternalId}/state`, {
method: "GET",
})

if (response.status === 404) {
return null
}

const text = await response.text()
if (!response.ok) {
throw new Error(`Polar customer state lookup failed (${response.status}): ${text.slice(0, 400)}`)
}

return text ? (JSON.parse(text) as PolarCustomerState) : null
}

function hasRequiredBenefit(state: PolarCustomerState | null) {
if (!state?.granted_benefits || !env.polar.benefitId) {
return false
}

return state.granted_benefits.some((grant) => grant.benefit_id === env.polar.benefitId)
}

async function createCheckoutSession(input: CloudAccessInput): Promise<string> {
const payload = {
products: [env.polar.productId],
success_url: env.polar.successUrl,
return_url: env.polar.returnUrl,
external_customer_id: input.userId,
customer_email: input.email,
customer_name: input.name,
}

const response = await polarFetch("/v1/checkouts/", {
method: "POST",
body: JSON.stringify(payload),
})
const text = await response.text()

if (!response.ok) {
throw new Error(`Polar checkout creation failed (${response.status}): ${text.slice(0, 400)}`)
}

const checkout = text ? (JSON.parse(text) as PolarCheckoutSession) : null
if (!checkout?.url) {
throw new Error("Polar checkout response missing URL")
}

return checkout.url
}

export async function requireCloudWorkerAccess(input: CloudAccessInput): Promise<CloudWorkerAccess> {
if (!env.polar.featureGateEnabled) {
return { allowed: true }
}

assertPaywallConfig()

const state = await getCustomerState(input.userId)
if (hasRequiredBenefit(state)) {
return { allowed: true }
}

const checkoutUrl = await createCheckoutSession(input)
return {
allowed: false,
checkoutUrl,
}
}
18 changes: 18 additions & 0 deletions services/den-control-plane/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ const schema = z.object({
RENDER_PROVISION_TIMEOUT_MS: z.string().optional(),
RENDER_HEALTHCHECK_TIMEOUT_MS: z.string().optional(),
RENDER_POLL_INTERVAL_MS: z.string().optional(),
POLAR_FEATURE_GATE_ENABLED: z.string().optional(),
POLAR_API_BASE: z.string().optional(),
POLAR_ACCESS_TOKEN: z.string().optional(),
POLAR_PRODUCT_ID: z.string().optional(),
POLAR_BENEFIT_ID: z.string().optional(),
POLAR_SUCCESS_URL: z.string().optional(),
POLAR_RETURN_URL: z.string().optional(),
})

const parsed = schema.parse(process.env)
Expand All @@ -29,6 +36,8 @@ const corsOrigins = parsed.CORS_ORIGINS?.split(",")
.map((origin) => origin.trim())
.filter(Boolean)

const polarFeatureGateEnabled = (parsed.POLAR_FEATURE_GATE_ENABLED ?? "false").toLowerCase() === "true"

export const env = {
databaseUrl: parsed.DATABASE_URL,
betterAuthSecret: parsed.BETTER_AUTH_SECRET,
Expand All @@ -52,4 +61,13 @@ export const env = {
healthcheckTimeoutMs: Number(parsed.RENDER_HEALTHCHECK_TIMEOUT_MS ?? "180000"),
pollIntervalMs: Number(parsed.RENDER_POLL_INTERVAL_MS ?? "5000"),
},
polar: {
featureGateEnabled: polarFeatureGateEnabled,
apiBase: parsed.POLAR_API_BASE ?? "https://api.polar.sh",
accessToken: parsed.POLAR_ACCESS_TOKEN,
productId: parsed.POLAR_PRODUCT_ID,
benefitId: parsed.POLAR_BENEFIT_ID,
successUrl: parsed.POLAR_SUCCESS_URL,
returnUrl: parsed.POLAR_RETURN_URL,
},
}
23 changes: 23 additions & 0 deletions services/den-control-plane/src/http/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { fromNodeHeaders } from "better-auth/node"
import { eq } from "drizzle-orm"
import { z } from "zod"
import { auth } from "../auth.js"
import { requireCloudWorkerAccess } from "../billing/polar.js"
import { db } from "../db/index.js"
import { OrgMembershipTable, WorkerInstanceTable, WorkerTable, WorkerTokenTable } from "../db/schema.js"
import { env } from "../env.js"
import { ensureDefaultOrg } from "../orgs.js"
import { provisionWorker } from "../workers/provisioner.js"

Expand Down Expand Up @@ -60,6 +62,27 @@ workersRouter.post("/", async (req, res) => {
return
}

if (parsed.data.destination === "cloud") {
const access = await requireCloudWorkerAccess({
userId: session.user.id,
email: session.user.email ?? `${session.user.id}@placeholder.local`,
name: session.user.name ?? session.user.email ?? "OpenWork User",
})

if (!access.allowed) {
res.status(402).json({
error: "payment_required",
message: "Cloud workers require an active Den Cloud plan.",
polar: {
checkoutUrl: access.checkoutUrl,
productId: env.polar.productId,
benefitId: env.polar.benefitId,
},
})
return
}
}

const orgId =
(await getOrgId(session.user.id)) ?? (await ensureDefaultOrg(session.user.id, session.user.name ?? session.user.email ?? "Personal"))
const workerId = randomUUID()
Expand Down
Loading