feat: implement raycast backend
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Bearer-token verification (Phase 1.3 — in-memory only).
|
||||
|
||||
Phase 4 will replace this with a DB-backed store (PRD §8 ``tokens``
|
||||
table, Argon2 hashes, scopes, ``last_used_at`` batching). Until then,
|
||||
frontends authenticate callers against an in-memory ``{value: name}``
|
||||
dict seeded from the ``BOOTSTRAP_TOKENS`` env var.
|
||||
|
||||
Format::
|
||||
|
||||
BOOTSTRAP_TOKENS=cursor:s3cret,laptop:hunter2
|
||||
|
||||
The *name* side is for audit lines — :py:meth:`TokenStore.verify`
|
||||
returns it on hit so callers can attribute the request without
|
||||
exposing the raw secret. ``None`` means "no such token"; callers
|
||||
turn that into 401.
|
||||
|
||||
This module deliberately knows nothing about HTTP frameworks — it
|
||||
takes a raw token (or the verbatim ``Authorization`` header value)
|
||||
and returns a name-or-None. Frontends own the response shape.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
class TokenStoreError(ValueError):
|
||||
"""Malformed ``BOOTSTRAP_TOKENS`` value."""
|
||||
|
||||
|
||||
class TokenStore:
|
||||
"""Constant-time-ish bearer verification over a static name→value map.
|
||||
|
||||
The store is keyed by value internally (``verify`` lookup is O(1))
|
||||
but constructed from a name→value mapping because that's how the
|
||||
user thinks about it — one human-readable label per caller.
|
||||
"""
|
||||
|
||||
__slots__ = ("_by_value",)
|
||||
|
||||
def __init__(self, tokens: Mapping[str, str]) -> None:
|
||||
by_value: dict[str, str] = {}
|
||||
for name, value in tokens.items():
|
||||
if not name or not value:
|
||||
msg = f"empty name or value in token map (name={name!r})"
|
||||
raise TokenStoreError(msg)
|
||||
if value in by_value:
|
||||
msg = (
|
||||
f"duplicate token value for names "
|
||||
f"{by_value[value]!r} and {name!r}"
|
||||
)
|
||||
raise TokenStoreError(msg)
|
||||
by_value[value] = name
|
||||
self._by_value = by_value
|
||||
|
||||
@classmethod
|
||||
def from_env(cls, raw: str) -> TokenStore:
|
||||
"""Parse ``name1:value1,name2:value2`` (the ``BOOTSTRAP_TOKENS`` form)."""
|
||||
tokens: dict[str, str] = {}
|
||||
for chunk in raw.split(","):
|
||||
entry = chunk.strip()
|
||||
if not entry:
|
||||
continue
|
||||
name, sep, value = entry.partition(":")
|
||||
if not sep:
|
||||
msg = f"token entry missing ':' separator: {entry!r}"
|
||||
raise TokenStoreError(msg)
|
||||
name, value = name.strip(), value.strip()
|
||||
if name in tokens:
|
||||
msg = f"duplicate token name: {name!r}"
|
||||
raise TokenStoreError(msg)
|
||||
tokens[name] = value
|
||||
return cls(tokens)
|
||||
|
||||
def verify(self, token: str | None) -> str | None:
|
||||
"""Return the token's name if known, else ``None``."""
|
||||
if not token:
|
||||
return None
|
||||
return self._by_value.get(token)
|
||||
|
||||
def verify_bearer(self, authorization: str | None) -> str | None:
|
||||
"""Strip the ``Bearer`` prefix (case-insensitive) then verify.
|
||||
|
||||
Accepts a bare token too — Cursor's MCP transport sometimes
|
||||
passes the raw value via ``?token=`` and reuses the same
|
||||
verifier; treating an unprefixed header as a bare token keeps
|
||||
both call sites on one method.
|
||||
"""
|
||||
if not authorization:
|
||||
return None
|
||||
head, sep, rest = authorization.partition(" ")
|
||||
token = rest.strip() if sep and head.lower() == "bearer" else authorization
|
||||
return self.verify(token)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._by_value)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._by_value)
|
||||
|
||||
|
||||
__all__ = ["TokenStore", "TokenStoreError"]
|
||||
Reference in New Issue
Block a user