mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-01 15:49:00 +08:00
8abca2f88c
Collapse the two error conventions (string-reason `{ok:false,reason}` outcomes
and thrown domain-error classes) onto one. Usecases now produce typed `AppError`
values via factories (`notFound()`/`quotaExceeded()`/`featureBlocked()`/…);
handlers `throw result.error`; and `jsonError` (renamed from `renderError`) is the
single place that renders any error to an AIP-193 body + access-log line, in
`app.onError`/accessLog.
Why: the previous setup had a string→code mapping (`outcomeError` + the `OUTCOME`
table) living in parallel with a type→code mapping (`mapDomainError`), plus inline
`apiError(c, <status>, …)` calls that hand-wrote the status at every site — exactly
the drift that left the same `quota_exceeded` at 400 in one handler and 422 in the
rest. Now the status/reason live once, in the factory.
- Add `server/usecases/ports/app-error.ts`: `AppError` + factories. Status/reason
are baked in per factory, so no usecase or handler writes an HTTP code or a
magic-string reason. `AppError` also carries optional response headers
(`Retry-After`) via a `rateLimited()` factory.
- Delete `apiError`, `outcomeError`, the `OUTCOME` table, and the dead `ApiError`
class. The 67 inline guard/middleware `apiError` sites became `throw <factory>()`.
- Control-flow outcomes a handler branches on (not just renders) stay discriminated
reasons (e.g. `deleteObject` `not_trashed`); internal shared sub-usecases
(traffic-metering, licensing internals) keep string reasons, mapped at the boundary.
- Regenerate the Go OpenAPI client (saveShare gained a 422 response).
BREAKING CHANGE: POST /shares/{token}/objects quota rejection now returns 422
(was an inconsistent 400); every other quota path already returned 422.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
import { release as osRelease } from 'node:os'
|
|
import { Hono } from 'hono'
|
|
import { constantTimeEqual } from '../lib/constant-time'
|
|
import type { Env } from '../middleware/platform'
|
|
import { getDeployPlatform } from '../runtime-platform'
|
|
import { notFound, unauthorized } from '../usecases/ports'
|
|
import { INSTANCE_TELEMETRY_CRON, reportInstanceTelemetry } from '../usecases/site/instance-telemetry'
|
|
|
|
const INTERNAL_API_TOKEN_ENV = 'ZPAN_INTERNAL_API_TOKEN'
|
|
|
|
const internal = new Hono<Env>()
|
|
|
|
function envAllowsIp(value: string | undefined): boolean {
|
|
return !['0', 'false', 'no', 'off'].includes(value?.trim().toLowerCase() ?? '')
|
|
}
|
|
|
|
internal.post('/instance-telemetry/report', async (c) => {
|
|
const platform = c.get('platform')
|
|
const token = platform.getEnv(INTERNAL_API_TOKEN_ENV)?.trim()
|
|
if (!token) throw notFound()
|
|
|
|
const auth = c.req.header('authorization') ?? ''
|
|
if (!constantTimeEqual(auth, `Bearer ${token}`)) throw unauthorized()
|
|
|
|
const runtime = platform.getBinding('DB')
|
|
? {
|
|
runtime: 'workerd' as const,
|
|
platform: 'cloudflare-workers' as const,
|
|
}
|
|
: {
|
|
runtime: 'node' as const,
|
|
platform: getDeployPlatform() ?? 'node',
|
|
osPlatform: process.platform,
|
|
osArch: process.arch,
|
|
osRelease: osRelease(),
|
|
nodeVersion: process.version,
|
|
}
|
|
|
|
const result = await reportInstanceTelemetry(c.get('deps'), {
|
|
config: {
|
|
allowIp: envAllowsIp(platform.getEnv('ZPAN_TELEMETRY_ALLOW_IP')),
|
|
},
|
|
cron: INSTANCE_TELEMETRY_CRON,
|
|
trigger: 'deploy',
|
|
runtime,
|
|
})
|
|
|
|
return c.json(result)
|
|
})
|
|
|
|
export default internal
|