diff --git a/lith/server.mjs b/lith/server.mjs --- a/lith/server.mjs +++ b/lith/server.mjs @@ -1260,6 +1260,7 @@ app.all("/m4l-plugins", directFn("m4l-plugins")); app.all("/slash", directFn("slash")); app.all("/sotce-blog/*rest", directFn("sotce-blog")); app.all("/profile/*rest", directFn("profile")); +app.all("/client/*rest", directFn("client-media")); // Menu Band crash-log intake → MongoDB collection "menuband-logs". Body is // the raw .ips text; metadata comes from headers. The text-body parser diff --git a/system/netlify/functions/client-media.mjs b/system/netlify/functions/client-media.mjs new file mode 100644 --- /dev/null +++ b/system/netlify/functions/client-media.mjs @@ -0,0 +1,124 @@ +// Capability-gated client media landing pages and fresh private downloads. +// +// Add a release to CLIENT_MEDIA with the SHA-256 of its unguessable access +// token. The raw token belongs only in the delivery message; it is never +// committed or logged. Assets remain private in Spaces. A successful download +// click receives a short-lived signed redirect generated at request time. + +import { createHash, timingSafeEqual } from "node:crypto"; +import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +const CLIENT_MEDIA = new Map([ + ["fia/jeannette-montgomery-barron", { + accessHash: "a3d63ea8a6fe63f3d3b093b46d75adfe451c0de05aa10c5891a1741e965aabb1", + title: "Jeannette Montgomery Barron Archive", + eyebrow: "Aesthetic Computer · Client media", + description: "A private, offline archive of posts, original videos, tagged work, captions, and a point-in-time follower snapshot.", + version: "1.1.0", + platform: "macOS · Apple Silicon + Intel", + size: "151.7 MB", + filename: "Jeannette-Montgomery-Barron-Archive-1.1.0.dmg", + bucket: "releases-aesthetic-computer", + objectKey: "clients/fia/jeannette-montgomery-barron/Jeannette-Montgomery-Barron-Archive-1.1.0.dmg", + ogImage: "https://releases.aesthetic.computer/clients/fia/jeannette-montgomery-barron/og-v1.png", + }], +]); + +let s3; + +function client() { + if (s3) return s3; + const accessKeyId = process.env.SPACES_KEY || process.env.DO_SPACES_KEY || process.env.ART_KEY; + const secretAccessKey = process.env.SPACES_SECRET || process.env.DO_SPACES_SECRET || process.env.ART_SECRET; + const rawEndpoint = process.env.SPACES_ENDPOINT || process.env.ART_ENDPOINT || "sfo3.digitaloceanspaces.com"; + const endpoint = rawEndpoint.startsWith("http") ? rawEndpoint : `https://${rawEndpoint}`; + if (!accessKeyId || !secretAccessKey) throw new Error("client-media storage credentials unavailable"); + s3 = new S3Client({ + endpoint, + region: "us-east-1", + credentials: { accessKeyId, secretAccessKey }, + requestChecksumCalculation: "WHEN_REQUIRED", + responseChecksumValidation: "WHEN_REQUIRED", + }); + return s3; +} + +function escape(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + })[character]); +} + +function pathParts(path = "") { + const marker = "/client/"; + const suffix = path.includes(marker) ? path.slice(path.indexOf(marker) + marker.length) : ""; + return suffix.split("/").filter(Boolean).map(decodeURIComponent); +} + +function authorized(token, expectedHash) { + if (!token || typeof token !== "string") return false; + const actual = Buffer.from(createHash("sha256").update(token).digest("hex")); + const expected = Buffer.from(expectedHash); + return actual.length === expected.length && timingSafeEqual(actual, expected); +} + +function response(statusCode, body, contentType = "text/plain; charset=utf-8", headers = {}) { + return { + statusCode, + headers: { + "Content-Type": contentType, + "Cache-Control": "private, no-store", + "X-Robots-Tag": "noindex, nofollow, noarchive", + "Referrer-Policy": "no-referrer", + ...headers, + }, + body, + }; +} + +function landing(entry, token, event, key) { + const origin = event.headers?.["x-forwarded-proto"] && event.headers?.host + ? `${event.headers["x-forwarded-proto"]}://${event.headers.host}` + : "https://aesthetic.computer"; + const pagePath = `/client/${key}`; + const pageURL = `${origin}${pagePath}?access=${encodeURIComponent(token)}`; + const downloadURL = `${pagePath}/download?access=${encodeURIComponent(token)}`; + const title = escape(entry.title); + const description = escape(entry.description); + return response(200, ` + +${title} — private download + + + + + + + + +
JMB archive mark

${escape(entry.eyebrow)}

${title}

${description}

Version ${escape(entry.version)}${escape(entry.platform)}${escape(entry.size)}Signed & notarized
Download for Mac
`, "text/html; charset=utf-8"); +} + +export async function handler(event) { + if (event.httpMethod !== "GET") return response(405, "Method not allowed"); + const parts = pathParts(event.path); + const key = parts.slice(0, 2).join("/"); + const entry = CLIENT_MEDIA.get(key); + if (!entry) return response(404, "Client media not found"); + const token = event.queryStringParameters?.access || ""; + if (!authorized(token, entry.accessHash)) return response(404, "Client media not found"); + if (parts[2] !== "download") return landing(entry, token, event, key); + try { + const url = await getSignedUrl(client(), new GetObjectCommand({ + Bucket: entry.bucket, + Key: entry.objectKey, + ResponseContentType: "application/x-apple-diskimage", + ResponseContentDisposition: `attachment; filename="${entry.filename}"`, + }), { expiresIn: 15 * 60 }); + return response(302, "", "text/plain; charset=utf-8", { Location: url }); + } catch (error) { + console.error("client-media download signing failed", error?.message || error); + return response(503, "Download temporarily unavailable"); + } +}