sojorn/_legacy/supabase/functions/_shared/r2_signer.ts
2026-02-15 00:33:24 -06:00

109 lines
3.4 KiB
TypeScript

import { AwsClient } from 'https://esm.sh/aws4fetch@1.0.17'
const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.sojorn.net").trim();
const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.sojorn.net").trim();
const DEFAULT_BUCKET_NAME = "sojorn-media";
const RESOLVED_BUCKET = (Deno.env.get("R2_BUCKET_NAME") ?? DEFAULT_BUCKET_NAME).trim();
function normalizeKey(key: string): string {
let normalized = key.replace(/^\/+/, "");
if (RESOLVED_BUCKET && normalized.startsWith(`${RESOLVED_BUCKET}/`)) {
normalized = normalized.slice(RESOLVED_BUCKET.length + 1);
}
return normalized;
}
function extractObjectKey(input: string): string {
const trimmed = input.trim();
if (!trimmed) {
throw new Error("Missing file key");
}
try {
const url = new URL(trimmed);
const key = decodeURIComponent(url.pathname);
return normalizeKey(key);
} catch {
return normalizeKey(trimmed);
}
}
export function transformLegacyMediaUrl(input: string): string | null {
const trimmed = input.trim();
if (!trimmed) return null;
try {
const url = new URL(trimmed);
// Handle legacy media.sojorn.net URLs
if (url.hostname === 'media.sojorn.net') {
const key = decodeURIComponent(url.pathname);
return key;
}
return null;
} catch {
return null;
}
}
// Deprecated: no-op signer retained for compatibility
export async function signR2Url(fileKey: string, expiresIn: number = 3600): Promise<string> {
return await trySignR2Url(fileKey, undefined, expiresIn) ?? fileKey;
}
export async function trySignR2Url(fileKey: string, bucket?: string, expiresIn: number = 3600): Promise<string | null> {
try {
const key = normalizeKey(extractObjectKey(fileKey));
// Check if we have credentials to sign. If not, fallback to public URL.
const ACCOUNT_ID = Deno.env.get('R2_ACCOUNT_ID');
const ACCESS_KEY = Deno.env.get('R2_ACCESS_KEY');
const SECRET_KEY = Deno.env.get('R2_SECRET_KEY');
const isVideo = key.toLowerCase().endsWith('.mp4') ||
key.toLowerCase().endsWith('.mov') ||
key.toLowerCase().endsWith('.webm') ||
bucket === 'sojorn-videos';
if (!ACCOUNT_ID || !ACCESS_KEY || !SECRET_KEY) {
console.warn("Missing R2 credentials for signing. Falling back to public domain.");
const domain = isVideo ? CUSTOM_VIDEO_DOMAIN : CUSTOM_MEDIA_DOMAIN;
if (domain && domain.startsWith("http")) {
return `${domain.replace(/\/+$/, "")}/${key}`;
}
return fileKey;
}
const r2 = new AwsClient({
accessKeyId: ACCESS_KEY,
secretAccessKey: SECRET_KEY,
region: 'auto',
service: 's3',
});
const targetBucket = bucket || (isVideo ? 'sojorn-videos' : 'sojorn-media');
// We sign against the actual R2 endpoint to ensure auth works,
// but the SignedMediaImage can handle redirect/proxying if needed.
const url = new URL(`https://${ACCOUNT_ID}.r2.cloudflarestorage.com/${targetBucket}/${key}`);
// Add expiration
url.searchParams.set('X-Amz-Expires', expiresIn.toString());
const signedRequest = await r2.sign(url, {
method: "GET",
aws: { signQuery: true, allHeaders: false },
});
return signedRequest.url;
} catch (error) {
console.error("R2 signing failed", {
fileKey,
error: error instanceof Error ? error.message : String(error),
});
return null;
}
}