feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)

New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
This commit is contained in:
Rotimi Best
2026-06-07 18:43:05 +01:00
committed by GitHub
parent a2e70779a0
commit d5b07d356e
496 changed files with 19050 additions and 8312 deletions
+1
View File
@@ -0,0 +1 @@
export * from './transcripts';
@@ -0,0 +1,101 @@
/**
* Transcript VTT proxy.
*
* `<track>` elements inside `<video crossorigin="anonymous">` can only
* load resources that return CORS headers. We serve the VTT through the
* api the same way we do HLS segments, rendering it on the fly from the
* stored transcript segments (the structured source of truth) so edits
* are reflected immediately without an R2 round trip. The path is
* same-origin behind the tenant-router Worker in prod and a CORS-allowed
* cross-origin call in local dev.
*
* Auth mirrors `/hls/*`:
* 1. Valid `cio_hls` HMAC cookie whose `aid` matches the assetId, OR
* 2. Authenticated session with org-member access to the asset's org.
*/
import { getAssetById } from '@cio/db/queries/assets';
import { getMediaTranscriptByAsset } from '@cio/db/queries';
import { segmentsToWebVtt } from '@cio/utils/functions';
import { HLS_COOKIE_NAME, verifyHlsToken } from '@cio/core/services/assets/assets';
import { resolveTrustedBrowserOriginForCors } from '@api/utils/origins';
import { getCookie } from 'hono/cookie';
import { Hono } from '@api/utils/hono';
export const transcriptsRouter = new Hono()
.options('/:assetId/track.vtt', (c) => {
const origin = c.req.header('origin');
let corsHeaders: Record<string, string> = {};
if (origin) {
const allowed = resolveTrustedBrowserOriginForCors(origin);
if (allowed) {
corsHeaders = {
'Access-Control-Allow-Origin': allowed,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '600',
'Cross-Origin-Resource-Policy': 'cross-origin'
};
}
}
return new Response(null, { status: 204, headers: corsHeaders });
})
/**
* GET/HEAD /transcripts/:assetId/track.vtt — render a transcript VTT
* from the stored segments with cross-origin CORS so a `<track crossorigin>`
* element in the dashboard can load it.
*/
.on(['GET', 'HEAD'], '/:assetId/track.vtt', async (c) => {
const assetId = c.req.param('assetId');
if (!assetId) return c.json({ success: false, message: 'Missing assetId' }, 400);
const origin = c.req.header('origin');
let corsHeaders: Record<string, string> = {};
if (origin) {
const allowed = resolveTrustedBrowserOriginForCors(origin);
if (allowed) {
corsHeaders = {
'Access-Control-Allow-Origin': allowed,
'Access-Control-Allow-Credentials': 'true',
'Cross-Origin-Resource-Policy': 'cross-origin'
};
}
}
const withCors = (body: BodyInit | null, status: number, extraHeaders?: Record<string, string>) =>
new Response(body, { status, headers: { ...corsHeaders, ...extraHeaders } });
const asset = await getAssetById(assetId);
if (!asset) return withCors(null, 404);
let authorized = false;
const token = getCookie(c, HLS_COOKIE_NAME);
if (token) {
const verified = await verifyHlsToken(token);
if (verified?.assetId === assetId) authorized = true;
}
if (!authorized) {
const orgRoles = (c.get('orgRoles') as Record<string, number> | undefined) ?? {};
if (orgRoles[asset.organizationId]) authorized = true;
}
if (!authorized) {
return withCors(null, 403);
}
const transcript = await getMediaTranscriptByAsset(assetId, asset.organizationId);
if (!transcript) return withCors(null, 404);
const vttHeaders = {
'Content-Type': 'text/vtt; charset=utf-8',
'Cache-Control': 'private, no-cache'
};
if (c.req.method === 'HEAD') {
return withCors(null, 200, vttHeaders);
}
const vtt = segmentsToWebVtt(transcript.segments);
return withCors(vtt, 200, vttHeaders);
});