Skip to content

Commit cda8c1b

Browse files
feat: add repository-scoped access tokens
1 parent 9e2e32d commit cda8c1b

22 files changed

Lines changed: 1591 additions & 72 deletions

File tree

docs/api-reference/sourcebot-public.openapi.json

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
"name": "Git",
1919
"description": "Git history, diff, and file content endpoints."
2020
},
21+
{
22+
"name": "Scoped Access Tokens",
23+
"description": "Mint and revoke short-lived credentials restricted to specific repositories."
24+
},
2125
{
2226
"name": "System",
2327
"description": "System health and version endpoints."
@@ -1090,6 +1094,61 @@
10901094
"$ref": "#/components/schemas/PublicCommitAuthor"
10911095
}
10921096
},
1097+
"PublicCreateScopedAccessTokenResponse": {
1098+
"type": "object",
1099+
"properties": {
1100+
"id": {
1101+
"type": "string",
1102+
"description": "Identifier used to revoke the token."
1103+
},
1104+
"token": {
1105+
"type": "string",
1106+
"pattern": "^sbst_",
1107+
"description": "Opaque bearer token. This value is returned only when the token is created."
1108+
},
1109+
"createdAt": {
1110+
"type": "string",
1111+
"format": "date-time"
1112+
},
1113+
"expiresAt": {
1114+
"type": "string",
1115+
"format": "date-time"
1116+
},
1117+
"repos": {
1118+
"type": "array",
1119+
"items": {
1120+
"type": "string",
1121+
"minLength": 1
1122+
},
1123+
"minItems": 1
1124+
}
1125+
},
1126+
"required": [
1127+
"id",
1128+
"token",
1129+
"createdAt",
1130+
"expiresAt",
1131+
"repos"
1132+
]
1133+
},
1134+
"PublicCreateScopedAccessTokenRequest": {
1135+
"type": "object",
1136+
"properties": {
1137+
"repos": {
1138+
"type": "array",
1139+
"items": {
1140+
"type": "string",
1141+
"minLength": 1
1142+
},
1143+
"minItems": 1,
1144+
"description": "Repository names to bind to the token. Every name must identify exactly one repository accessible to the API-key owner."
1145+
}
1146+
},
1147+
"required": [
1148+
"repos"
1149+
],
1150+
"additionalProperties": false
1151+
},
10931152
"PublicEeUser": {
10941153
"type": "object",
10951154
"properties": {
@@ -1253,7 +1312,7 @@
12531312
"bearerToken": {
12541313
"type": "http",
12551314
"scheme": "bearer",
1256-
"description": "Bearer authentication header of the form `Bearer <token>`, where `<token>` is your API key."
1315+
"description": "Bearer authentication header of the form `Bearer <token>`. The token may be a Sourcebot API key, OAuth access token, or scoped access token, subject to endpoint requirements."
12571316
},
12581317
"apiKeyHeader": {
12591318
"type": "apiKey",
@@ -2266,6 +2325,161 @@
22662325
}
22672326
}
22682327
},
2328+
"/api/ee/scoped_access_token": {
2329+
"post": {
2330+
"operationId": "createScopedAccessToken",
2331+
"tags": [
2332+
"Scoped Access Tokens"
2333+
],
2334+
"summary": "Create a scoped access token",
2335+
"description": "Creates an opaque bearer token that expires exactly one hour after issuance and is restricted to the requested repositories. Repository names are resolved atomically against the API-key owner's current access; the request fails if any name is missing, inaccessible, or ambiguous.\n\nThis endpoint requires a Sourcebot API key. Scoped access tokens, OAuth tokens, and browser sessions cannot mint another scoped access token. The returned token is independent of the API key after issuance and cannot be refreshed.",
2336+
"security": [
2337+
{
2338+
"bearerToken": []
2339+
},
2340+
{
2341+
"apiKeyHeader": []
2342+
}
2343+
],
2344+
"requestBody": {
2345+
"required": true,
2346+
"content": {
2347+
"application/json": {
2348+
"schema": {
2349+
"$ref": "#/components/schemas/PublicCreateScopedAccessTokenRequest"
2350+
}
2351+
}
2352+
}
2353+
},
2354+
"responses": {
2355+
"201": {
2356+
"description": "Scoped access token created. The opaque token value is returned only in this response.",
2357+
"content": {
2358+
"application/json": {
2359+
"schema": {
2360+
"$ref": "#/components/schemas/PublicCreateScopedAccessTokenResponse"
2361+
}
2362+
}
2363+
}
2364+
},
2365+
"400": {
2366+
"description": "Invalid request body or repository scope.",
2367+
"content": {
2368+
"application/json": {
2369+
"schema": {
2370+
"$ref": "#/components/schemas/PublicApiServiceError"
2371+
}
2372+
}
2373+
}
2374+
},
2375+
"401": {
2376+
"description": "Missing or invalid authentication.",
2377+
"content": {
2378+
"application/json": {
2379+
"schema": {
2380+
"$ref": "#/components/schemas/PublicApiServiceError"
2381+
}
2382+
}
2383+
}
2384+
},
2385+
"403": {
2386+
"description": "The current authentication method is not an API key, or the API-key owner is not permitted to perform this operation.",
2387+
"content": {
2388+
"application/json": {
2389+
"schema": {
2390+
"$ref": "#/components/schemas/PublicApiServiceError"
2391+
}
2392+
}
2393+
}
2394+
},
2395+
"500": {
2396+
"description": "Unexpected token creation failure.",
2397+
"content": {
2398+
"application/json": {
2399+
"schema": {
2400+
"$ref": "#/components/schemas/PublicApiServiceError"
2401+
}
2402+
}
2403+
}
2404+
}
2405+
}
2406+
}
2407+
},
2408+
"/api/ee/scoped_access_token/{id}": {
2409+
"delete": {
2410+
"operationId": "revokeScopedAccessToken",
2411+
"tags": [
2412+
"Scoped Access Tokens"
2413+
],
2414+
"summary": "Revoke a scoped access token",
2415+
"description": "Immediately revokes a scoped access token created by the authenticated API-key owner. This endpoint requires a Sourcebot API key.",
2416+
"security": [
2417+
{
2418+
"bearerToken": []
2419+
},
2420+
{
2421+
"apiKeyHeader": []
2422+
}
2423+
],
2424+
"parameters": [
2425+
{
2426+
"schema": {
2427+
"type": "string",
2428+
"description": "Identifier returned when the scoped access token was created."
2429+
},
2430+
"required": true,
2431+
"description": "Identifier returned when the scoped access token was created.",
2432+
"name": "id",
2433+
"in": "path"
2434+
}
2435+
],
2436+
"responses": {
2437+
"204": {
2438+
"description": "Scoped access token revoked."
2439+
},
2440+
"401": {
2441+
"description": "Missing or invalid authentication.",
2442+
"content": {
2443+
"application/json": {
2444+
"schema": {
2445+
"$ref": "#/components/schemas/PublicApiServiceError"
2446+
}
2447+
}
2448+
}
2449+
},
2450+
"403": {
2451+
"description": "The current authentication method is not an API key, or the API-key owner is not permitted to perform this operation.",
2452+
"content": {
2453+
"application/json": {
2454+
"schema": {
2455+
"$ref": "#/components/schemas/PublicApiServiceError"
2456+
}
2457+
}
2458+
}
2459+
},
2460+
"404": {
2461+
"description": "Scoped access token not found.",
2462+
"content": {
2463+
"application/json": {
2464+
"schema": {
2465+
"$ref": "#/components/schemas/PublicApiServiceError"
2466+
}
2467+
}
2468+
}
2469+
},
2470+
"500": {
2471+
"description": "Unexpected token revocation failure.",
2472+
"content": {
2473+
"application/json": {
2474+
"schema": {
2475+
"$ref": "#/components/schemas/PublicApiServiceError"
2476+
}
2477+
}
2478+
}
2479+
}
2480+
}
2481+
}
2482+
},
22692483
"/api/ee/user": {
22702484
"get": {
22712485
"operationId": "getUser",

docs/docs.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@
189189
"GET /api/repos"
190190
]
191191
},
192+
{
193+
"group": "Scoped Access Tokens",
194+
"icon": "key",
195+
"pages": [
196+
"POST /api/ee/scoped_access_token",
197+
"DELETE /api/ee/scoped_access_token/{id}"
198+
]
199+
},
192200
{
193201
"group": "Git",
194202
"icon": "code-branch",

docs/docs/api-reference/authentication.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,22 @@ curl -X POST https://your-sourcebot-instance.com/api/search \
3232
-H "Content-Type: application/json" \
3333
-d '{"query": "hello world", "matches": 10}'
3434
```
35+
36+
## Using a scoped access token
37+
38+
Scoped access tokens are short-lived bearer credentials intended for clients that should only access a specific set of repositories. Create one with a Sourcebot API key by calling `POST /api/ee/scoped_access_token` with repository names:
39+
40+
```bash
41+
curl -X POST https://your-sourcebot-instance.com/api/ee/scoped_access_token \
42+
-H "Authorization: Bearer <your-api-key>" \
43+
-H "Content-Type: application/json" \
44+
-d '{"repos": ["github.com/acme/frontend", "github.com/acme/backend"]}'
45+
```
46+
47+
The response contains an opaque token beginning with `sbst_`. It expires exactly one hour after issuance, cannot be refreshed, and is returned only once. Use it as a Bearer token with public API endpoints or the Sourcebot MCP server:
48+
49+
```bash
50+
Authorization: Bearer <your-scoped-access-token>
51+
```
52+
53+
Repository scope is bound internally to repository IDs and is also intersected with the creating user's current repository permissions. Creating and revoking scoped access tokens requires an API key; a scoped access token cannot mint or revoke tokens.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- CreateTable
2+
CREATE TABLE "ScopedAccessToken" (
3+
"id" TEXT NOT NULL,
4+
"hash" TEXT NOT NULL,
5+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
"expiresAt" TIMESTAMP(3) NOT NULL,
7+
"lastUsedAt" TIMESTAMP(3),
8+
"createdById" TEXT NOT NULL,
9+
"orgId" INTEGER NOT NULL,
10+
11+
CONSTRAINT "ScopedAccessToken_pkey" PRIMARY KEY ("id")
12+
);
13+
14+
-- CreateTable
15+
CREATE TABLE "ScopedAccessTokenToRepo" (
16+
"tokenId" TEXT NOT NULL,
17+
"repoId" INTEGER NOT NULL,
18+
19+
CONSTRAINT "ScopedAccessTokenToRepo_pkey" PRIMARY KEY ("tokenId","repoId")
20+
);
21+
22+
-- CreateIndex
23+
CREATE UNIQUE INDEX "ScopedAccessToken_hash_key" ON "ScopedAccessToken"("hash");
24+
25+
-- CreateIndex
26+
CREATE INDEX "ScopedAccessToken_createdById_orgId_expiresAt_idx" ON "ScopedAccessToken"("createdById", "orgId", "expiresAt");
27+
28+
-- CreateIndex
29+
CREATE INDEX "ScopedAccessToken_expiresAt_idx" ON "ScopedAccessToken"("expiresAt");
30+
31+
-- CreateIndex
32+
CREATE INDEX "ScopedAccessTokenToRepo_repoId_idx" ON "ScopedAccessTokenToRepo"("repoId");
33+
34+
-- AddForeignKey
35+
ALTER TABLE "ScopedAccessToken" ADD CONSTRAINT "ScopedAccessToken_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
36+
37+
-- AddForeignKey
38+
ALTER TABLE "ScopedAccessToken" ADD CONSTRAINT "ScopedAccessToken_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org"("id") ON DELETE CASCADE ON UPDATE CASCADE;
39+
40+
-- AddForeignKey
41+
ALTER TABLE "ScopedAccessTokenToRepo" ADD CONSTRAINT "ScopedAccessTokenToRepo_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES "ScopedAccessToken"("id") ON DELETE CASCADE ON UPDATE CASCADE;
42+
43+
-- AddForeignKey
44+
ALTER TABLE "ScopedAccessTokenToRepo" ADD CONSTRAINT "ScopedAccessTokenToRepo_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE;

packages/db/prisma/schema.prisma

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ model Repo {
7171
defaultBranch String?
7272
7373
permittedAccounts AccountToRepoPermission[]
74+
scopedAccessTokens ScopedAccessTokenToRepo[]
7475
permissionSyncJobs RepoPermissionSyncJob[]
7576
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
7677
@@ -286,6 +287,7 @@ model Org {
286287
connections Connection[]
287288
repos Repo[]
288289
apiKeys ApiKey[]
290+
scopedAccessTokens ScopedAccessToken[]
289291
scimTokens ScimToken[]
290292
attachments Attachment[]
291293
isOnboarded Boolean @default(false)
@@ -454,6 +456,37 @@ model ApiKey {
454456
createdById String
455457
}
456458

459+
model ScopedAccessToken {
460+
id String @id @default(cuid())
461+
hash String @unique
462+
463+
createdAt DateTime @default(now())
464+
expiresAt DateTime
465+
lastUsedAt DateTime?
466+
467+
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
468+
createdById String
469+
470+
org Org @relation(fields: [orgId], references: [id], onDelete: Cascade)
471+
orgId Int
472+
473+
repos ScopedAccessTokenToRepo[]
474+
475+
@@index([createdById, orgId, expiresAt])
476+
@@index([expiresAt])
477+
}
478+
479+
model ScopedAccessTokenToRepo {
480+
token ScopedAccessToken @relation(fields: [tokenId], references: [id], onDelete: Cascade)
481+
tokenId String
482+
483+
repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
484+
repoId Int
485+
486+
@@id([tokenId, repoId])
487+
@@index([repoId])
488+
}
489+
457490
/// Org-scoped bearer token presented by an IdP (Okta, Entra) to authenticate
458491
/// against the SCIM provisioning endpoints. Unlike `ApiKey`, a SCIM token is
459492
/// not tied to a user — it acts on behalf of the SCIM integration for the
@@ -509,6 +542,7 @@ model User {
509542
invites Invite[]
510543
511544
apiKeys ApiKey[]
545+
scopedAccessTokens ScopedAccessToken[]
512546
513547
chats Chat[]
514548
sharedChats ChatAccess[]

packages/shared/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const API_KEY_PREFIX = 'sbk_';
1212
export const OAUTH_ACCESS_TOKEN_PREFIX = 'sboa_';
1313
export const OAUTH_REFRESH_TOKEN_PREFIX = 'sbor_';
1414
export const SCIM_TOKEN_PREFIX = 'sbscim_';
15+
export const SCOPED_ACCESS_TOKEN_PREFIX = 'sbst_';
1516

1617
/**
1718
* Default settings.

0 commit comments

Comments
 (0)