mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-31 02:04:40 +08:00
fbec74747e
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>
88 lines
3.6 KiB
TypeScript
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))
|
|
}
|