interloper-api 0.40.0__tar.gz → 0.43.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {interloper_api-0.40.0 → interloper_api-0.43.3}/PKG-INFO +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/pyproject.toml +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/app.py +12 -10
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/agent.py +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/backfills.py +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/catalog.py +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/components.py +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/runs.py +1 -1
- interloper_api-0.43.3/src/interloper_api/routes/tokens.py +120 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/ws.py +1 -1
- {interloper_api-0.40.0 → interloper_api-0.43.3}/README.md +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/__init__.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/components.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/dependencies.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/email.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/__init__.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/admin.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/auth.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/oauth.py +0 -0
- {interloper_api-0.40.0 → interloper_api-0.43.3}/src/interloper_api/routes/organisations.py +0 -0
|
@@ -21,6 +21,7 @@ from interloper_api.routes import (
|
|
|
21
21
|
oauth,
|
|
22
22
|
organisations,
|
|
23
23
|
runs,
|
|
24
|
+
tokens,
|
|
24
25
|
ws,
|
|
25
26
|
)
|
|
26
27
|
from interloper_api.routes import catalog as catalog_routes
|
|
@@ -88,22 +89,23 @@ def create_app(
|
|
|
88
89
|
set_smtp_config(smtp_config)
|
|
89
90
|
|
|
90
91
|
api = APIRouter(prefix="/api")
|
|
91
|
-
api.include_router(auth.router
|
|
92
|
-
api.include_router(organisations.router
|
|
93
|
-
api.include_router(admin.router
|
|
94
|
-
api.include_router(catalog_routes.router
|
|
95
|
-
api.include_router(components.router
|
|
96
|
-
api.include_router(runs.router
|
|
97
|
-
api.include_router(backfills.router
|
|
98
|
-
api.include_router(oauth.router
|
|
99
|
-
api.include_router(
|
|
92
|
+
api.include_router(auth.router)
|
|
93
|
+
api.include_router(organisations.router)
|
|
94
|
+
api.include_router(admin.router)
|
|
95
|
+
api.include_router(catalog_routes.router)
|
|
96
|
+
api.include_router(components.router)
|
|
97
|
+
api.include_router(runs.router)
|
|
98
|
+
api.include_router(backfills.router)
|
|
99
|
+
api.include_router(oauth.router)
|
|
100
|
+
api.include_router(tokens.router)
|
|
101
|
+
api.include_router(ws.router)
|
|
100
102
|
|
|
101
103
|
agent_available = False
|
|
102
104
|
if agent_config is None or agent_config.enabled:
|
|
103
105
|
try:
|
|
104
106
|
from interloper_api.routes import agent as agent_routes
|
|
105
107
|
|
|
106
|
-
api.include_router(agent_routes.router
|
|
108
|
+
api.include_router(agent_routes.router)
|
|
107
109
|
agent_available = True
|
|
108
110
|
except ImportError:
|
|
109
111
|
logger.warning(
|
|
@@ -51,7 +51,7 @@ from interloper_api.dependencies import (
|
|
|
51
51
|
|
|
52
52
|
logger = logging.getLogger(__name__)
|
|
53
53
|
|
|
54
|
-
router = APIRouter()
|
|
54
|
+
router = APIRouter(prefix="/components", tags=["components"])
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
# -- Request/Response models --------------------------------------------------
|
|
@@ -22,7 +22,7 @@ from interloper_api.dependencies import (
|
|
|
22
22
|
require_viewer,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
router = APIRouter()
|
|
25
|
+
router = APIRouter(prefix="/runs", tags=["runs"])
|
|
26
26
|
|
|
27
27
|
#: Hard cap on the number of events returned in a single page, regardless of
|
|
28
28
|
#: the requested ``limit``. Keeps a pathological ``?limit=1000000`` from loading
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Personal access token routes — mint, list, and revoke API tokens.
|
|
2
|
+
|
|
3
|
+
Tokens authenticate programmatic clients (the MCP server, CLIs) as their
|
|
4
|
+
holder in one organisation, with the holder's live role. Management is
|
|
5
|
+
session-cookie-only by design: a leaked token must not be able to mint
|
|
6
|
+
further tokens.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
15
|
+
from interloper_db import Profile, Store
|
|
16
|
+
from pydantic import BaseModel, Field
|
|
17
|
+
|
|
18
|
+
from interloper_api.dependencies import get_current_user, get_org_id, get_store, require_viewer
|
|
19
|
+
|
|
20
|
+
router = APIRouter(prefix="/tokens", tags=["tokens"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# -- Response / Request models ------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CreateTokenRequest(BaseModel):
|
|
27
|
+
"""Request body for creating a token."""
|
|
28
|
+
|
|
29
|
+
name: str = Field(min_length=1, max_length=100)
|
|
30
|
+
expires_in_days: int | None = Field(default=90, ge=1, le=3650)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TokenResponse(BaseModel):
|
|
34
|
+
"""Token metadata — never carries secret material."""
|
|
35
|
+
|
|
36
|
+
id: UUID
|
|
37
|
+
name: str
|
|
38
|
+
token_prefix: str
|
|
39
|
+
organisation_id: UUID
|
|
40
|
+
created_at: datetime | None = None
|
|
41
|
+
expires_at: datetime | None = None
|
|
42
|
+
last_used_at: datetime | None = None
|
|
43
|
+
revoked_at: datetime | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CreatedTokenResponse(TokenResponse):
|
|
47
|
+
"""Creation response: the only place the raw token ever appears."""
|
|
48
|
+
|
|
49
|
+
token: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# -- Routes --------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("", status_code=201)
|
|
56
|
+
def create_token(
|
|
57
|
+
body: CreateTokenRequest,
|
|
58
|
+
user: Profile = Depends(require_viewer),
|
|
59
|
+
org_id: UUID = Depends(get_org_id),
|
|
60
|
+
store: Store = Depends(get_store),
|
|
61
|
+
) -> CreatedTokenResponse:
|
|
62
|
+
"""Create a personal access token scoped to the active organisation.
|
|
63
|
+
|
|
64
|
+
Any org member may mint one: the token conveys only the holder's own
|
|
65
|
+
live role, so no escalation is possible. The raw token is returned
|
|
66
|
+
exactly once and cannot be recovered afterwards.
|
|
67
|
+
"""
|
|
68
|
+
expires_at = None
|
|
69
|
+
if body.expires_in_days is not None:
|
|
70
|
+
expires_at = datetime.now(timezone.utc) + timedelta(days=body.expires_in_days)
|
|
71
|
+
|
|
72
|
+
row, raw = store.create_token(user.id, org_id, name=body.name, expires_at=expires_at)
|
|
73
|
+
return CreatedTokenResponse(
|
|
74
|
+
id=row.id,
|
|
75
|
+
name=row.name,
|
|
76
|
+
token_prefix=row.token_prefix,
|
|
77
|
+
organisation_id=row.organisation_id,
|
|
78
|
+
created_at=row.created_at,
|
|
79
|
+
expires_at=row.expires_at,
|
|
80
|
+
last_used_at=row.last_used_at,
|
|
81
|
+
revoked_at=row.revoked_at,
|
|
82
|
+
token=raw,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get("")
|
|
87
|
+
def list_tokens(
|
|
88
|
+
user: Profile = Depends(require_viewer),
|
|
89
|
+
org_id: UUID = Depends(get_org_id),
|
|
90
|
+
store: Store = Depends(get_store),
|
|
91
|
+
) -> list[TokenResponse]:
|
|
92
|
+
"""List the caller's tokens in the active organisation."""
|
|
93
|
+
rows = store.list_tokens(user.id, org_id)
|
|
94
|
+
return [TokenResponse.model_validate(row, from_attributes=True) for row in rows]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.delete("/{token_id}")
|
|
98
|
+
def revoke_token(
|
|
99
|
+
token_id: UUID,
|
|
100
|
+
user: Profile = Depends(get_current_user),
|
|
101
|
+
store: Store = Depends(get_store),
|
|
102
|
+
) -> dict[str, str]:
|
|
103
|
+
"""Revoke a token.
|
|
104
|
+
|
|
105
|
+
The owner may revoke their own tokens; an org admin may revoke any token
|
|
106
|
+
scoped to their organisation. Missing and unauthorized get the same 404,
|
|
107
|
+
so token IDs don't act as an existence oracle.
|
|
108
|
+
"""
|
|
109
|
+
detail = f"Token {token_id} not found"
|
|
110
|
+
row = store.get_token(token_id)
|
|
111
|
+
if row is None:
|
|
112
|
+
raise HTTPException(status_code=404, detail=detail)
|
|
113
|
+
|
|
114
|
+
if row.user_id != user.id:
|
|
115
|
+
role = store.get_user_role(user.id, row.organisation_id)
|
|
116
|
+
if role != "admin":
|
|
117
|
+
raise HTTPException(status_code=404, detail=detail)
|
|
118
|
+
|
|
119
|
+
store.revoke_token(token_id)
|
|
120
|
+
return {"status": "revoked"}
|
|
@@ -24,7 +24,7 @@ from fastapi import APIRouter, Cookie, FastAPI, WebSocket, WebSocketDisconnect
|
|
|
24
24
|
from interloper_api.dependencies import get_store
|
|
25
25
|
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
|
-
router = APIRouter()
|
|
27
|
+
router = APIRouter(tags=["ws"])
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class ConnectionManager:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|