feat(embeds): add embed.classroomio.com public widget host

Serve widgets from short public URLs via tenant-router with R2-backed
assets and runtime API_URL injection, so copy-paste snippets hide infra
paths. Update widget payloads, CSP, upload purge URLs, and dev-only API
inject; document format/stage workflow in AGENTS.md and fix ui-prefix-check
YAML syntax.
This commit is contained in:
Rotimi Best
2026-06-12 02:06:37 +01:00
parent c868b2f23f
commit 367ddcddd7
23 changed files with 399 additions and 62 deletions
+2
View File
@@ -7,3 +7,5 @@ export const TENANT_ROOT_DOMAIN = 'myclassroomio.com';
/** The marketing / admin / api zone. */
export const BRAND_ROOT_DOMAIN = 'classroomio.com';
export { EMBED_PUBLIC_BASE_URL, EMBED_PUBLIC_HOST } from './embeds';
+56
View File
@@ -0,0 +1,56 @@
export const EMBED_PUBLIC_HOST = 'embed.classroomio.com';
export const EMBED_PUBLIC_BASE_URL = `https://${EMBED_PUBLIC_HOST}`;
export type EmbedWidgetRoute = {
/** R2 object key for the ES module bootstrap. */
scriptKey: string;
/** R2 object key for the hosted iframe page, when supported. */
pageKey?: string;
/** Query param that selects the hosted page instead of the script. */
pageQueryParam?: string;
};
/**
* Public path segment → internal R2 keys under the `assets` bucket.
* Customers only copy `https://embed.classroomio.com/{widgetName}`.
*/
export const EMBED_WIDGET_ROUTES = {
'course-widget': {
scriptKey: 'embeds/course-widget/course-widget.js',
pageKey: 'embeds/course-widget/embed.html',
pageQueryParam: 'key'
},
'question-type-picker': {
scriptKey: 'embeds/question-type-picker/question-type-picker.js'
}
} as const satisfies Record<string, EmbedWidgetRoute>;
export type EmbedWidgetName = keyof typeof EMBED_WIDGET_ROUTES;
export function getEmbedBaseUrl(override?: string): string {
return (override ?? EMBED_PUBLIC_BASE_URL).replace(/\/$/, '');
}
export function getEmbedPublicUrl(
widgetName: EmbedWidgetName,
params?: Record<string, string | undefined>,
baseUrlOverride?: string
): string {
const baseUrl = getEmbedBaseUrl(baseUrlOverride);
const path = `${baseUrl}/${widgetName}`;
if (!params) {
return path;
}
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value != null && value !== '') {
search.set(key, value);
}
}
const query = search.toString();
return query ? `${path}?${query}` : path;
}
+1
View File
@@ -2,5 +2,6 @@ export * from './roles';
export * from './org';
export * from './content';
export * from './domains';
export * from './embeds';
export * from './error-codes';
export * from './agent';
+2
View File
@@ -13,6 +13,8 @@ export const blockedSubdomain = [
'courses',
'demo',
'dev',
'embed',
'embeds',
'forum',
'group',
'groups',