Skip to main content
所有路由处理器在 src/routes.ts,装配在 src/index.ts。公网入口 https://authapi.kira.art(内部端口 8095)。

路由概览

MethodPath用途CORS
GET/login?next=&invite=校验 next 白名单 → 暂存 next/invite(host cookie)→ signInWithOAuth(google) → 302 Google顶层导航,无
GET/callback?code=exchangeCodeForSession → onboarding → 封印 refresh 进 .kira.art cookie → 302 next顶层导航,无
POST/refreshka_session → Supabase 轮换 → 回写 cookie → 返回 { access_token, ... }白名单 + credentials
POST/logout用 cookie refresh 换 access → Supabase logout(global) → 清 cookie。幂等白名单 + credentials
GET/health健康检查
GET/服务标识

CORS 与 credentials

/login/callback 是浏览器顶层导航,无需 CORS。/refresh/logout 是前端跨源 fetch(需带 credentials 以携带 .kira.art cookie),装了 CORS 中间件:
cors({
  origin: env.ALLOWED_REDIRECT_ORIGINS,          // 白名单 origin(非 *)
  credentials: true,                              // 回显具体 origin,放行 cookie
  allowMethods: ["POST", "OPTIONS"],
  allowHeaders: ["Content-Type", "traceparent", "tracestate", "baggage"],
})
credentials: true 时 ACAO 必须回显具体 origin(不能是 *),Hono 用数组(白名单)形式自动处理。traceparent / tracestate / baggage 是 W3C trace context —— kira-web 的 fetch instrumentation 会给自家服务注入,必须在 preflight 放行。

GET /login

发起 Google OAuth。 Query 参数
参数必填说明
next登录完成后的回跳目标。必须落在 ALLOWED_REDIRECT_ORIGINS 内,否则用 DEFAULT_REDIRECT_ORIGIN 兜底
invite邀请人 user id,用于 onboarding referral
行为
  1. resolveNext(next) 白名单校验。
  2. setCookie(ka_next),有 invite 则 setCookie(ka_invite)(均 host-only + httpOnly + SameSite=Lax,maxAge=600s)。
  3. signInWithOAuth({ provider: "google", options: { redirectTo: ${PUBLIC_BASE_URL}/callback, skipBrowserRedirect: true } })
  4. 302 到 Google 授权页。
错误
情况响应
signInWithOAuth 返回 error 或无 url502 { message: "oauth_init_failed" }(appLog.error)

GET /callback

Google 回跳端点。必须在 Supabase Auth → URL Configuration → Redirect URLs 注册(prod https://authapi.kira.art/callback,dev http://localhost:8095/callback)。 Query 参数
参数必填说明
codeGoogle 授权码,用于 exchangeCodeForSession
行为
  1. code302DEFAULT_REDIRECT_ORIGIN
  2. exchangeCodeForSession(code)。失败 → 302${DEFAULT_REDIRECT_ORIGIN}/?auth_error=1
  3. 取回并清掉 ka_next / ka_invite
  4. runOnboarding(user, invite, accept-language)(referral / language)。
  5. setSessionCookie(refresh_token) —— 封印进 .kira.art ka_session(maxAge=400d)。
  6. 302next。referral 成功且有 invite 时,追加 ?referral=success&inviter=<invite>
access token 不进 URL。前端落地后必须经 POST /refresh 拿 access。

POST /refresh

前端获取 access token 的唯一途径。无请求体(refresh 来自 ka_session cookie)。 请求
POST /refresh
Origin: https://creator.kira.art
Cookie: ka_session=<sealed>      # 浏览器在 credentialed fetch 自动带上 .kira.art cookie
成功响应 200
{
  "access_token": "<supabase-signed-jwt>",
  "token_type": "bearer",
  "expires_in": 3600,
  "expires_at": 1730000000
}
同时 Set-Cookie: ka_session=<新封印>(refresh 已轮换)。 错误状态 /refresh 严格区分「会话失效」与「Supabase 暂时不可达」——核心是 refreshGrant() 的三态返回,绝不能把抖动当登出:
情况refreshGrant 状态HTTPbodycookie日志
无 / 无法解封的 ka_session401{ error: "no_session" }不动debug
新 token 拿到ok200{ access_token, ... }回写新封印debug
refresh 真失效(过期/已用/reuse 撤销,Supabase 4xx)expired401{ error: "refresh_failed" }清掉死 cookiedebug(常见会话到期)
Supabase 暂时不可达(网络错 / 5xx)unavailable503{ error: "upstream_unavailable" }不清(refresh 还有效)warn(真异常,上 Dash0)
unavailable 故意回 503不清 cookie,让前端稍后重试、不要登出。此前 bare fetch 无 try/catch:Supabase 不可达会 reject → 冒到 onError500 → 前端当 auth 失败把用户登出 → 一次 Supabase 抖动 = 全员掉线。三态返回就是修这个坑。

POST /logout

幂等登出。无请求体。 行为
  1. ka_session 的 refresh;有则 refreshGrant() 换 access,再 revokeSession(access, "global")(POST /auth/v1/logout?scope=global,对齐 kira-web signOut 默认 global)。
  2. 无论吊销成功与否(best-effort,失败仅 warn)→ clearSessionCookie().kira.art cookie。
响应 200
{ "ok": true }

GET /health · GET /

无认证的标识端点(src/index.ts inline)。
Path响应
GET /health{ "status": "ok", "service": "kira-auth" }
GET /{ "service": "kira-auth" }

错误 span 语义(src/index.ts)

@hono/otel 在 handler 抛错时把 SERVER span 标 ERROR(含 4xx HTTPException)。按 OTel HTTP 语义,4xx SERVER 不该是 ERROR。中间件在 @hono/otel 内侧吃掉 <500HTTPException 转 response,不让它回到 @hono/otel 的 catch;5xx + 未捕获错误仍往上传到 onErrorcaptureException(ERROR log + span.recordException)。详见 Deployment 的 Observability 小节
app.use(async (c, next) => {
  try {
    await next();
  } catch (e) {
    if (e instanceof HTTPException && e.status < 500) {
      return e.getResponse();   // 4xx:转 response,不标 ERROR span
    }
    throw e;                     // 5xx:上传 onError → captureException
  }
});

GoTrue 转发(src/lib/gotrue.ts)

/refresh/logout 不走 supabase-js client(refreshSession 会 mutate client 内部状态、并发 race),直接 fetch GoTrue REST:
操作调用Headers
刷新POST ${SUPABASE_URL}/auth/v1/token?grant_type=refresh_tokenapikey: $SUPABASE_ANON_KEY,Authorization: Bearer $SUPABASE_ANON_KEY,body { refresh_token }
吊销POST ${SUPABASE_URL}/auth/v1/logout?scope=globalapikey: $SUPABASE_ANON_KEY,Authorization: Bearer <access_token>