feat(frontends,security,api,ui): a frontend can hold a token scope of its own

This commit is contained in:
hh
2026-09-06 23:26:26 +02:00
parent 5401cbe187
commit d5aaa80822
11 changed files with 245 additions and 21 deletions
+5
View File
@@ -12,6 +12,7 @@ import type {
LimitsResponse,
MemoryFile,
MemoryTree,
ScopeRow,
SearchResponse,
SessionsResponse,
TokenRow,
@@ -351,6 +352,10 @@ export class ApiClient {
});
}
scopes(): Promise<{ scopes: ScopeRow[] }> {
return this.get("/api/scopes");
}
createToken(
name: string,
scope: string
+6
View File
@@ -132,6 +132,7 @@ export interface FrontendInfo {
kinds: Kind[];
name: string;
path: string | null;
scope: string | null;
type: string;
url: string | null;
}
@@ -245,6 +246,11 @@ export interface MemoryFile {
size: number;
}
export interface ScopeRow {
frontend: string | null;
scope: string;
}
export interface TokenRow {
created_at: string;
id: number;
+11 -1
View File
@@ -117,7 +117,17 @@
};
default:
return {
endpoints: base ? [{ hint: "", label: "base", url: base }] : [],
endpoints: base
? [
{
hint: fe.scope
? `POST the frontend's own payload; bearer with scope ${fe.scope}`
: "",
label: "base",
url: base,
},
]
: [],
frontend: fe,
note: "",
};
+34 -9
View File
@@ -2,7 +2,7 @@
import CopyIcon from "@lucide/svelte/icons/copy";
import PlusIcon from "@lucide/svelte/icons/plus";
import { toast } from "svelte-sonner";
import type { TokenRow } from "$lib/api/types";
import type { ScopeRow, TokenRow } from "$lib/api/types";
import EmptyState from "$lib/components/empty-state.svelte";
import ErrorNote from "$lib/components/error-note.svelte";
import { Button } from "$lib/components/ui/button";
@@ -15,13 +15,23 @@
import { session } from "$lib/session.svelte";
import { cn } from "$lib/utils";
const SCOPES = [
{ hint: "every frontend", value: "*" },
{ hint: "conversations API and SSE (panel, plugin)", value: "api" },
{ hint: "Anthropic /v1/messages", value: "messages" },
{ hint: "MCP server", value: "mcp" },
{ hint: "tokens and audit over the API", value: "admin" },
];
const HINTS: Record<string, string> = {
"*": "every frontend",
admin: "tokens and audit over the API",
api: "conversations API and SSE (panel, plugin)",
mcp: "MCP server",
messages: "Anthropic /v1/messages",
};
let scopes = $state<ScopeRow[]>([]);
const options = $derived(
scopes.map((row) => ({
hint: row.frontend
? `the ${row.frontend} frontend`
: (HINTS[row.scope] ?? ""),
value: row.scope,
}))
);
let tokens = $state<TokenRow[] | null>(null);
let includeRevoked = $state(false);
@@ -49,6 +59,21 @@
load(includeRevoked);
});
$effect(() => {
const { client } = session;
if (!client) {
return;
}
client
.scopes()
.then(({ scopes: rows }) => {
scopes = rows;
})
.catch(() => {
scopes = [];
});
});
async function create() {
const { client } = session;
if (!client) {
@@ -254,7 +279,7 @@
>
<Select.Trigger class="w-full">{scope}</Select.Trigger>
<Select.Content>
{#each SCOPES as option (option.value)}
{#each options as option (option.value)}
<Select.Item label={option.value} value={option.value}>
<span class="flex flex-col">
<span>{option.value}</span>