mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-21 13:20:33 +08:00
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>
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
import { createMiddleware } from 'hono/factory'
|
|
import type { Auth } from '../auth'
|
|
import type { Platform } from '../platform/interface'
|
|
import type { Deps } from '../usecases/deps'
|
|
|
|
export type Env = {
|
|
Variables: {
|
|
platform: Platform
|
|
auth: Auth
|
|
deps: Deps
|
|
principal: AuthPrincipal | null
|
|
userId: string | null
|
|
userRole: string | null
|
|
orgId: string | null
|
|
// Structured detail for the access log on a failed request. Set by `jsonError`
|
|
// (via `app.onError`); read by the accessLog middleware so every 4xx/5xx carries
|
|
// its reason + full message, not just unhandled crashes.
|
|
errorLog: { reason: string; message: string } | null
|
|
}
|
|
}
|
|
|
|
export type AuthPrincipal =
|
|
| {
|
|
kind: 'user'
|
|
userId: string
|
|
role?: string
|
|
orgId: string | null
|
|
authMethod: 'cookie' | 'bearer'
|
|
}
|
|
| {
|
|
kind: 'api-key'
|
|
keyId: string
|
|
configId: string
|
|
orgId: string | null
|
|
userId: string | null
|
|
permissions: Record<string, string[]> | null
|
|
authMethod: 'api-key'
|
|
}
|
|
| {
|
|
kind: 'downloader'
|
|
downloaderId: string
|
|
authMethod: 'bearer'
|
|
}
|
|
| {
|
|
kind: 'download-task-upload'
|
|
downloaderId: string
|
|
taskId: string
|
|
orgId: string
|
|
targetFolder: string
|
|
createdByUserId: string
|
|
scopes: string[]
|
|
authMethod: 'bearer'
|
|
}
|
|
|
|
export const platformMiddleware = (platform: Platform, auth: Auth) =>
|
|
createMiddleware<Env>(async (c, next) => {
|
|
c.set('platform', platform)
|
|
c.set('auth', auth)
|
|
c.set('principal', null)
|
|
c.set('errorLog', null)
|
|
await next()
|
|
})
|