Skip to content

feat: add transcript download endpoint to developer REST API (#1656) - #2092

Open
Samarth1306w wants to merge 8 commits into
CapSoftware:mainfrom
Samarth1306w:feat/add-transcript-endpoint-to-developer-rest-api-1656
Open

feat: add transcript download endpoint to developer REST API (#1656)#2092
Samarth1306w wants to merge 8 commits into
CapSoftware:mainfrom
Samarth1306w:feat/add-transcript-endpoint-to-developer-rest-api-1656

Conversation

@Samarth1306w

@Samarth1306w Samarth1306w commented Aug 6, 2026

Copy link
Copy Markdown

Fixes #1656

Summary

Add endpoint to the developer REST API ().

  1. ****:
    • Queries table using the authenticated and secret key middleware.
    • Returns if video is not found or soft-deleted.
    • Returns if .
    • Returns if (e.g. or ).
    • Returns with transcript metadata (uid=1000(codespace) gid=1000(codespace) groups=1000(codespace),985(pipx),986(python),987(oryx),988(golang),989(docker),990(sdkman),991(rvm),992(php),993(conda),994(nvs),995(nvm),996(hugo),1001(ssh), , ) when complete.

Greptile Summary

This PR adds a developer transcript endpoint alongside CLI recording metadata lookup, video metadata retrieval, configurable OpenAI-compatible endpoints, rate limits, tray shortcut labels, and teleprompter playback fixes.

  • Adds /videos/:id/transcript to the authenticated developer API.
  • Adds cap recordings info and a supporting video metadata GET route.
  • Adds rate limiting to analytics tracking and guest checkout.
  • Adds OPENAI_BASE_URL support to AI generation and messenger flows.
  • Updates desktop tray hotkey labels and desktop/mobile teleprompter state handling.

Confidence Score: 1/5

The PR is not safe to merge until transcript delivery, metadata authorization, and CLI share-URL parsing are corrected.

The completed-transcript response cannot provide transcript bytes, the metadata route discloses password-protected AI content, and valid share-URL variants fail in the new CLI command.

Files Needing Attention: apps/web/app/api/developer/v1/[...route]/videos.ts, apps/web/app/api/video/metadata/route.ts, apps/cli/src/recordings.rs

Security Review

The new video metadata GET bypasses the canonical video-view policy, allowing unauthenticated callers to retrieve AI summaries and chapters for public videos that are password-protected.

Important Files Changed

Filename Overview
apps/web/app/api/developer/v1/[...route]/videos.ts Adds the developer transcript endpoint, but its successful response exposes the uploaded-video key rather than transcript content or a transcript URL.
apps/web/app/api/video/metadata/route.ts Adds metadata retrieval with an incomplete authorization check that bypasses password protection, plus an untyped ad-hoc API contract.
apps/cli/src/recordings.rs Adds remote recording metadata lookup, but simplistic share-URL parsing rejects valid URLs with trailing slashes or query parameters.
apps/cli/src/main.rs Registers and asynchronously dispatches the new recordings info command.
apps/web/app/api/analytics/track/route.ts Adds request rate limiting before analytics payload processing.
apps/web/app/api/settings/billing/guest-checkout/route.ts Adds request rate limiting before Stripe guest checkout creation.
apps/web/lib/messenger/agent.ts Routes OpenAI-compatible messenger requests through the optional configured base URL.
apps/web/workflows/generate-ai.ts Routes AI summary generation through the optional OpenAI-compatible base URL.
packages/env/server.ts Adds the optional OPENAI_BASE_URL server environment variable.
apps/desktop/src-tauri/src/hotkeys.rs Exposes hotkey fields, formats accelerators, and refreshes the tray after hotkey updates.
apps/desktop/src-tauri/src/tray.rs Displays configured keyboard accelerators alongside tray recording and screenshot actions.
apps/desktop/src/routes/teleprompter.tsx Preserves the desktop teleprompter scroll position while resizing before playback.
apps/mobile/src/recording/TeleprompterOverlay.tsx Preserves mobile teleprompter progress across subsequent text layout changes.
apps/web/tests/unit/rate-limit-ids.test.ts Adds a source-scanning contract test for active rate-limit identifier references.
Prompt To Fix All With AI
### Issue 1
apps/web/app/api/developer/v1/[...route]/videos.ts:158-164
**Transcript response returns video key**

When transcription is complete, this endpoint returns `developerVideos.s3Key`, which identifies the raw uploaded video rather than the separately stored `transcription.vtt` object. Because the response includes neither transcript content nor a signed transcript URL, API consumers cannot download the completed transcript.

### Issue 2
apps/web/app/api/video/metadata/route.ts:60-62
**Public flag bypasses password policy**

When a public video is password-protected, this direct `public` check grants unauthenticated access without invoking `VideosPolicy.canView`, exposing its AI title, summary, and chapters without the configured password. **How this was verified:** The canonical view policy verifies passwords for public videos, while this handler returns metadata after checking only `video.public` and ownership.

### Issue 3
apps/cli/src/recordings.rs:111-124
**Share URL parsing mangles IDs**

When a valid share URL has a trailing slash or query parameters, `rsplit('/').next()` produces an empty ID or retains the query string in the ID. The metadata request then receives an invalid `videoId` and returns 400 or 404 for an otherwise valid video.

### Issue 4
apps/web/app/api/video/metadata/route.ts:42-64
**Metadata route bypasses typed API conventions**

The new GET endpoint is an ad-hoc Next.js handler and casts persisted metadata to `Record<string, any>`, bypassing the repository's required `HttpApi` contract pattern and type narrowing. This leaves its request, response, and metadata fields vulnerable to contract drift.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat: add transcript download endpoint t..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

Context used (6)

@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

Comment on lines +158 to +164
return c.json({
data: {
id: video.id,
transcriptionStatus: video.transcriptionStatus,
s3Key: video.s3Key,
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Transcript response returns video key

When transcription is complete, this endpoint returns developerVideos.s3Key, which identifies the raw uploaded video rather than the separately stored transcription.vtt object. Because the response includes neither transcript content nor a signed transcript URL, API consumers cannot download the completed transcript.

Knowledge Base Used: Infra, Storage and Config

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/api/developer/v1/[...route]/videos.ts
Line: 158-164

Comment:
**Transcript response returns video key**

When transcription is complete, this endpoint returns `developerVideos.s3Key`, which identifies the raw uploaded video rather than the separately stored `transcription.vtt` object. Because the response includes neither transcript content nor a signed transcript URL, API consumers cannot download the completed transcript.

**Knowledge Base Used:** [Infra, Storage and Config](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/infra-storage-config.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +60 to +62
if (!video.public && video.ownerId !== user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Public flag bypasses password policy

When a public video is password-protected, this direct public check grants unauthenticated access without invoking VideosPolicy.canView, exposing its AI title, summary, and chapters without the configured password. How this was verified: The canonical view policy verifies passwords for public videos, while this handler returns metadata after checking only video.public and ownership.

Knowledge Base Used: Web App (apps/web)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/api/video/metadata/route.ts
Line: 60-62

Comment:
**Public flag bypasses password policy**

When a public video is password-protected, this direct `public` check grants unauthenticated access without invoking `VideosPolicy.canView`, exposing its AI title, summary, and chapters without the configured password. **How this was verified:** The canonical view policy verifies passwords for public videos, while this handler returns metadata after checking only `video.public` and ownership.

**Knowledge Base Used:** [Web App (apps/web)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/web-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +111 to +124
let video_id = if url_or_id.contains('/') {
url_or_id
.rsplit('/')
.next()
.unwrap_or(&url_or_id)
.to_string()
} else {
url_or_id
};

let server_url = std::env::var("CAP_SERVER_URL")
.unwrap_or_else(|_| "https://cap.so".to_string());

let endpoint = format!("{}/api/video/metadata?videoId={}", server_url.trim_end_matches('/'), video_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Share URL parsing mangles IDs

When a valid share URL has a trailing slash or query parameters, rsplit('/').next() produces an empty ID or retains the query string in the ID. The metadata request then receives an invalid videoId and returns 400 or 404 for an otherwise valid video.

Knowledge Base Used: Cap CLI (apps/cli)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/cli/src/recordings.rs
Line: 111-124

Comment:
**Share URL parsing mangles IDs**

When a valid share URL has a trailing slash or query parameters, `rsplit('/').next()` produces an empty ID or retains the query string in the ID. The metadata request then receives an invalid `videoId` and returns 400 or 404 for an otherwise valid video.

**Knowledge Base Used:** [Cap CLI (`apps/cli`)](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/cli.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +42 to +64

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const videoId = searchParams.get("videoId");

if (!videoId) {
return Response.json({ error: "Missing videoId parameter" }, { status: 400 });
}

const query = await db().select().from(videos).where(eq(videos.id, videoId));

if (query.length === 0 || !query[0]) {
return Response.json({ error: "Video not found" }, { status: 404 });
}

const video = query[0];
const user = await getCurrentUser();

if (!video.public && video.ownerId !== user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}

const meta = (video.metadata as Record<string, any>) ?? {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Metadata route bypasses typed API conventions

The new GET endpoint is an ad-hoc Next.js handler and casts persisted metadata to Record<string, any>, bypassing the repository's required HttpApi contract pattern and type narrowing. This leaves its request, response, and metadata fields vulnerable to contract drift.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/api/video/metadata/route.ts
Line: 42-64

Comment:
**Metadata route bypasses typed API conventions**

The new GET endpoint is an ad-hoc Next.js handler and casts persisted metadata to `Record<string, any>`, bypassing the repository's required `HttpApi` contract pattern and type narrowing. This leaves its request, response, and metadata fields vulnerable to contract drift.

**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add transcript download endpoint to REST API

2 participants