File size: 2,701 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { verifyAccessToken } from "@/lib/db/accessTokens";
import { scopeSatisfies, type AccessScope } from "@/lib/accessTokens/scopes";
import { inferRequiredScope } from "@/server/authz/accessScopes";

/**
 * Shared evaluation of a scoped CLI access token (`oma_...`) for remote mode.
 *
 * Used by BOTH the central authz pipeline (`managementPolicy` β€” the authoritative
 * gate wired through `src/proxy.ts`) and the route-level `requireManagementAuth`
 * (defense-in-depth + lets `/api/cli/whoami` learn its own scope). Keeping the
 * logic here means the two gates can never drift.
 *
 * Returns a neutral verdict the caller maps to its own response shape
 * (`allow()/reject()` in the policy, `null`/Response in requireManagementAuth).
 */

/** Prefix that distinguishes a CLI access token from an inference API key. */
export const ACCESS_TOKEN_PREFIX = "oma_";

export type AccessTokenVerdict =
  | { kind: "absent" } // no oma_ bearer present β†’ caller continues other auth paths
  | { kind: "error" } // auth backend (DB) threw β†’ 503, not an auth failure
  | { kind: "invalid" } // oma_ present but unknown/expired/revoked β†’ 401
  | { kind: "insufficient"; have: AccessScope; need: AccessScope } // valid but scope too low β†’ 403
  | { kind: "ok"; scope: AccessScope; id: string; name: string }; // authorized β†’ allow

/** Read a Bearer token from the Authorization header (header-only; never URL). */
export function extractBearer(request: Request): string | null {
  const header = request.headers.get("authorization") || "";
  const match = /^Bearer\s+(.+)$/i.exec(header.trim());
  return match ? match[1].trim() : null;
}

function safePathname(url: string): string {
  try {
    return new URL(url).pathname;
  } catch {
    return "/";
  }
}

/**
 * Evaluate the request's access-token credential and required scope.
 * Pure w.r.t. the request (only side effect is `verifyAccessToken` stamping
 * `last_used_at`). Never throws β€” DB failures surface as `{ kind: "error" }`.
 */
export function evaluateAccessTokenAuth(request: Request): AccessTokenVerdict {
  const bearer = extractBearer(request);
  if (!bearer || !bearer.startsWith(ACCESS_TOKEN_PREFIX)) {
    return { kind: "absent" };
  }

  let verified: ReturnType<typeof verifyAccessToken>;
  try {
    verified = verifyAccessToken(bearer);
  } catch {
    return { kind: "error" };
  }
  if (!verified) return { kind: "invalid" };

  const need = inferRequiredScope(request.method, safePathname(request.url));
  if (!scopeSatisfies(verified.scope, need)) {
    return { kind: "insufficient", have: verified.scope, need };
  }

  return { kind: "ok", scope: verified.scope, id: verified.id, name: verified.name };
}