mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
export type CloudShareTicket = {
|
|
uploadUrl: string;
|
|
shareUrl: string;
|
|
shareCode?: string;
|
|
method: "PUT";
|
|
headers: Record<string, string>;
|
|
finalizeUrl?: string;
|
|
};
|
|
|
|
const TRUSTED_SHARE_ORIGINS = new Set([
|
|
"https://videos.recordly.dev",
|
|
"http://localhost:8787",
|
|
"http://127.0.0.1:8787",
|
|
]);
|
|
|
|
function parseUrl(value: unknown, label: string): URL {
|
|
if (typeof value !== "string") throw new Error(`${label} is missing.`);
|
|
const url = new URL(value);
|
|
if (url.username || url.password) throw new Error(`${label} cannot contain credentials.`);
|
|
return url;
|
|
}
|
|
|
|
export function normalizeCloudEndpoint(value: unknown): string {
|
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
throw new Error("Enter a cloud share endpoint.");
|
|
}
|
|
const url = parseUrl(value.trim(), "Cloud share endpoint");
|
|
if (!TRUSTED_SHARE_ORIGINS.has(url.origin)) {
|
|
throw new Error("Cloud sharing is only allowed through the Recordly service.");
|
|
}
|
|
if (url.pathname !== "/api/upload" || url.search) {
|
|
throw new Error("The cloud share endpoint is invalid.");
|
|
}
|
|
url.hash = "";
|
|
return url.toString();
|
|
}
|
|
|
|
function parseHttpUrl(value: unknown, label: string): string {
|
|
const url = parseUrl(value, `${label} from the server response`);
|
|
const localHttp =
|
|
url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1");
|
|
if (url.protocol !== "https:" && !localHttp) {
|
|
throw new Error(`${label} must use HTTPS (HTTP is allowed for localhost).`);
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
export function parseCloudShareTicket(value: unknown): CloudShareTicket {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error("The cloud share server returned an invalid response.");
|
|
}
|
|
const input = value as Record<string, unknown>;
|
|
const uploadUrl = input.uploadUrl ?? input.uploadURL;
|
|
const shareUrl = input.shareUrl ?? input.shareURL;
|
|
const shareCode =
|
|
typeof input.shareCode === "string" && /^[a-z0-9]+$/.test(input.shareCode)
|
|
? input.shareCode
|
|
: undefined;
|
|
const method = input.method === undefined ? "PUT" : input.method;
|
|
if (method !== "PUT") throw new Error("Only presigned PUT uploads are currently supported.");
|
|
const headers: Record<string, string> = {};
|
|
if (input.headers !== undefined) {
|
|
if (!input.headers || typeof input.headers !== "object" || Array.isArray(input.headers)) {
|
|
throw new Error("Upload headers must be an object.");
|
|
}
|
|
for (const [key, headerValue] of Object.entries(input.headers)) {
|
|
if (typeof headerValue !== "string")
|
|
throw new Error("Upload header values must be strings.");
|
|
if (
|
|
[
|
|
"authorization",
|
|
"cookie",
|
|
"host",
|
|
"content-length",
|
|
"proxy-authorization",
|
|
].includes(key.toLowerCase())
|
|
) {
|
|
throw new Error(`The upload header ${key} is not allowed.`);
|
|
}
|
|
headers[key] = headerValue;
|
|
}
|
|
}
|
|
const parsedUploadUrl = parseHttpUrl(uploadUrl, "uploadUrl");
|
|
const parsedShareUrl = parseHttpUrl(shareUrl, "shareUrl");
|
|
let finalizeUrl: string | undefined;
|
|
if (typeof input.finalizeUrl === "string") {
|
|
finalizeUrl = parseHttpUrl(input.finalizeUrl, "finalizeUrl");
|
|
} else if (typeof input.shareCode === "string" && /^[a-z0-9]+$/.test(input.shareCode)) {
|
|
// Voom marks a recording ready after metadata is posted.
|
|
finalizeUrl = new URL(`/api/metadata/${input.shareCode}`, parsedShareUrl).toString();
|
|
}
|
|
return {
|
|
uploadUrl: parsedUploadUrl,
|
|
shareUrl: parsedShareUrl,
|
|
shareCode,
|
|
method,
|
|
headers,
|
|
finalizeUrl,
|
|
};
|
|
}
|