CJ_Clippy 665b7ea924
Some checks failed
fp/our CI/CD / build (push) Successful in 2m15s
ci / test (push) Failing after 4m21s
signed urls allow querystrings
2025-11-14 17:11:07 -08:00

112 lines
3.8 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
/**
* onFileDownloadRequest hook is triggered before each API File download request. Could be used to validate or modify the file response before returning it to the client.
* @see https://pocketbase.io/docs/js-event-hooks/#onfiledownloadrequest
*
* We use this to return a 302 to the CDN asset instead of having the asset proxied via Pocketbase
* @see https://github.com/pocketbase/pocketbase/discussions/5995
*
*/
onFileDownloadRequest((event) => {
// console.log('event', JSON.stringify(event))
// e.app
// e.collection
// e.record
// e.fileField
// e.servedPath
// e.servedName
// and all RequestEvent fields...
const securityKey = process.env?.BUNNY_TOKEN_KEY;
const baseUrl = process.env?.BUNNY_ZONE_URL;
// console.log(`securityKey=${securityKey}, baseUrl=${baseUrl}`)
if (!securityKey) {
console.error('BUNNY_TOKEN_KEY was missing from env');
return event.next();
}
if (!baseUrl) {
console.error('BUNNY_ZONE_URL was missing from env');
return event.next();
}
/**
* Generate a signed BunnyCDN URL.
* @param {string} securityKey - Your BunnyCDN security token
* @param {string} baseUrl - The base URL (protocol + host)
* @param {string} path - Path to the file (starting with /)
* @param {string} rawQuery - Raw query string, e.g., "width=500&quality=5"
* @param {number} expires - Unix timestamp for expiration
*/
function signUrlCool(securityKey, baseUrl, path, rawQuery = "", expires) {
if (!path.startsWith('/')) path = '/' + path;
if (baseUrl.endsWith('/')) throw new Error(`baseUrl must not end with a slash. got baseUrl=${baseUrl}`);
// Build parameter string (sort keys alphabetically)
let parameterData = "";
if (rawQuery) {
const params = rawQuery
.split("&")
.map(p => p.split("="))
.filter(([key]) => key && key !== "token" && key !== "expires")
.sort(([a], [b]) => a.localeCompare(b));
if (params.length) {
parameterData = params.map(([k, v]) => `${k}=${v}`).join("&");
}
}
// Build hashable base
const hashableBase = securityKey + path + expires + parameterData;
// console.log(`hashableBase`, hashableBase)
// Compute token using your $security.sha256 workflow
const tokenH = $security.sha256(hashableBase);
const token = Buffer.from(tokenH, "hex")
.toString("base64")
.replace(/\n/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
// Build final signed URL
let tokenUrl = baseUrl + path + "?token=" + token;
if (parameterData) tokenUrl += "&" + parameterData;
tokenUrl += "&expires=" + expires;
return tokenUrl;
}
const rawQuery = event.requestEvent.request.url.rawQuery;
// console.log(`record: ${JSON.stringify(event.record)}`)
// // console.log(`collection: ${JSON.stringify(event.collection)}`)
// console.log(`app: ${JSON.stringify(event.app)}`)
// console.log(`fileField: ${JSON.stringify(event.fileField)}`)
// console.log(`servedPath: ${JSON.stringify(event.servedPath)}`)
// console.log(`servedName: ${JSON.stringify(event.servedName)}`)
// Our job here is to take the servedPath, and sign it using bunnycdn method
// Then serve a 302 redirect instead of serving the file proxied thru PB
const path = event.servedPath;
const expires = Math.round(Date.now() / 1000) + 3600;
const signedUrl = signUrlCool(securityKey, baseUrl, path, rawQuery, expires);
// console.log(`rawQUery`, rawQuery, 'path', path);
// console.log(`signedUrl=${signedUrl}`);
// This redirect is a tricky thing. We do this to avoid proxying file requests via our pocketbase origin server.
// The idea is to reduce load.
// HOWEVER, this redirect slows down image loading because it now takes 2 requests per image.
event.redirect(302, signedUrl);
event.next()
})