Files
zpan/server/http/licensing.ts
T
Jasper Van fbec74747e refactor(usecases): consolidate licensing into one file, merge tiny usecases, drop dead code (#434)
Group all license application logic into a single usecases/licensing.ts
(certificate/token verification, binding-state, cloud refresh, license-gated
policy) and collapse small single-purpose usecases that belonged together.

- delete license-entitlement.ts: a write-only cache nobody read (loadEntitlement
  had zero live consumers); remove its invalidate* call-sites
- merge licensing-refresh-runner -> license-refresh, then fold
  license-certificate + license-refresh + license-policy + binding-state into
  one licensing.ts (internal cert<-state<-refresh<-policy edges become in-file)
- merge team-count + signup-mode -> license-policy (then into licensing.ts)
- merge trash-retention -> purge (manual purge + scheduled retention sweep)

Tests follow the source: the runner tests are rewritten against a fake
LicensingCloud port (the old module-spy on performRefresh can't survive a
same-module call), and the team-limit test seeds a real pro license instead of
mocking the licensing module. Net 486+/861-. typecheck clean; unit+integration
green (158 files / 3797 tests).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:42:47 -04:00

88 lines
3.6 KiB
TypeScript

import { timingSafeEqual } from 'node:crypto'
import type { Context } from 'hono'
import { Hono } from 'hono'
import { ZPAN_CLOUD_URL_DEFAULT } from '../../shared/constants'
import type { BindingState } from '../../shared/types'
import { originFromRequestUrl } from '../domain/site-public-origin'
import type { Env } from '../middleware/platform'
import { syncPendingCloudTrafficReports } from '../usecases/cloud-traffic-metering'
import { buildCloudInstanceInfo, runtimeInfo } from '../usecases/instance-info'
import { loadBindingState, normalizeHost, runLicensingRefresh } from '../usecases/licensing'
import { syncPendingRemoteDownloadUsageReports } from '../usecases/remote-download-usage'
import { getSitePublicOrigin } from '../usecases/site-public-origin'
async function configuredPublicHost(c: Context<Env>): Promise<string | null> {
const origin = await getInstanceOrigin(c)
return origin ? new URL(origin).host : null
}
async function getInstanceOrigin(c: Context<Env>): Promise<string | null> {
return (await getSitePublicOrigin(c.get('deps'))) ?? originFromRequestUrl(c.req.url)
}
function cloudDashboardUrl(cloudBaseUrl: string): string {
return `${cloudBaseUrl.replace(/\/$/, '')}/dashboard`
}
function secretsMatch(provided: string, expected: string): boolean {
if (provided.length !== expected.length) return false
const enc = new TextEncoder()
return timingSafeEqual(enc.encode(provided), enc.encode(expected))
}
const app = new Hono<Env>()
.get('/status', async (c) => {
const cloudBaseUrl = c.get('platform').getEnv('ZPAN_CLOUD_URL') ?? ZPAN_CLOUD_URL_DEFAULT
const currentHost =
(await configuredPublicHost(c)) ??
normalizeHost(c.req.header('x-forwarded-host') ?? c.req.header('host')) ??
new URL(c.req.url).host
const state = await loadBindingState(c.get('deps'), { currentHost, cloudBaseUrl })
return c.json({ ...state, cloud_dashboard_url: cloudDashboardUrl(cloudBaseUrl) } satisfies BindingState)
})
// POST /api/licensing/refresh-cron?secret=<REFRESH_CRON_SECRET>
// External schedulers (Vercel Cron, Netlify Scheduled Functions, etc.) call
// this endpoint every 6 hours instead of running a native cron trigger.
// Set REFRESH_CRON_SECRET to a random string (e.g. openssl rand -hex 32)
// and pass it as the `secret` query parameter.
.post('/refresh-cron', async (c) => {
if (!isAuthorizedCronRequest(c)) {
return c.json({ error: 'Unauthorized' }, 401)
}
const cloudBaseUrl = c.get('platform').getEnv('ZPAN_CLOUD_URL') ?? ZPAN_CLOUD_URL_DEFAULT
const origin = await getInstanceOrigin(c)
const instance = origin
? await buildCloudInstanceInfo(c.get('deps'), {
url: origin,
runtime: runtimeInfo(c.get('platform')),
})
: undefined
await runLicensingRefresh(c.get('deps'), cloudBaseUrl, instance)
return c.json({ ok: true })
})
.post('/traffic-sync-runs', async (c) => {
if (!isAuthorizedCronRequest(c)) {
return c.json({ error: 'Unauthorized' }, 401)
}
const cloudBaseUrl = c.get('platform').getEnv('ZPAN_CLOUD_URL') ?? ZPAN_CLOUD_URL_DEFAULT
const [traffic, remoteDownload] = await Promise.all([
syncPendingCloudTrafficReports(c.get('deps'), { cloudBaseUrl }),
syncPendingRemoteDownloadUsageReports(c.get('deps'), { cloudBaseUrl }),
])
return c.json({ ok: true, ...traffic, remoteDownload })
})
export default app
function isAuthorizedCronRequest(c: Context<Env>) {
const expectedSecret = c.get('platform').getEnv('REFRESH_CRON_SECRET')
const provided = c.req.query('secret') ?? ''
return Boolean(expectedSecret && secretsMatch(provided, expectedSecret))
}