/** * OSS object download utility. * Uses the same credentials as STS to sign GET requests to Aliyun OSS. */ const crypto = require("node:crypto"); const OSS_ACCESS_KEY_ID = process.env.STS_ACCESS_KEY_ID || ""; const OSS_ACCESS_KEY_SECRET = process.env.STS_ACCESS_KEY_SECRET || ""; const OSS_BUCKET = process.env.OSS_BUCKET || ""; const OSS_REGION = process.env.OSS_REGION || ""; function getOssEndpoint() { return `https://${OSS_BUCKET}.${OSS_REGION}.aliyuncs.com`; } function isOssConfigured() { return !!(OSS_ACCESS_KEY_ID && OSS_ACCESS_KEY_SECRET && OSS_BUCKET && OSS_REGION); } function buildCanonicalOssHeaders(headers = {}) { return Object.entries(headers) .filter(([key]) => key.toLowerCase().startsWith("x-oss-")) .map(([key, value]) => [key.toLowerCase(), String(value).trim()]) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => `${key}:${value}\n`) .join(""); } function signOssRequest(method, objectKey, date, contentType = "", extraHeaders = {}) { const contentMd5 = ""; const canonicalResource = `/${OSS_BUCKET}/${objectKey}`; const canonicalHeaders = buildCanonicalOssHeaders(extraHeaders); const stringToSign = `${method}\n${contentMd5}\n${contentType}\n${date}\n${canonicalHeaders}${canonicalResource}`; const signature = crypto .createHmac("sha1", OSS_ACCESS_KEY_SECRET) .update(stringToSign) .digest("base64"); return `OSS ${OSS_ACCESS_KEY_ID}:${signature}`; } function createSignedReadUrl(objectKey, expiresInSeconds = 24 * 60 * 60) { if (!isOssConfigured()) { throw new Error("OSS is not configured"); } const expires = Math.floor(Date.now() / 1000) + expiresInSeconds; const canonicalResource = `/${OSS_BUCKET}/${objectKey}`; const stringToSign = `GET\n\n\n${expires}\n${canonicalResource}`; const signature = crypto .createHmac("sha1", OSS_ACCESS_KEY_SECRET) .update(stringToSign) .digest("base64"); const encodedKey = encodeURIComponent(objectKey).replace(/%2F/g, "/"); const query = new URLSearchParams({ OSSAccessKeyId: OSS_ACCESS_KEY_ID, Expires: String(expires), Signature: signature, }); return `${getOssEndpoint()}/${encodedKey}?${query.toString()}`; } async function getObject(objectKey) { if (!isOssConfigured()) { throw new Error("OSS is not configured"); } const date = new Date().toUTCString(); const authorization = signOssRequest("GET", objectKey, date); const url = `${getOssEndpoint()}/${encodeURIComponent(objectKey).replace(/%2F/g, "/")}`; const response = await fetch(url, { method: "GET", headers: { Date: date, Authorization: authorization }, }); if (!response.ok) { const text = await response.text().catch(() => ""); const error = new Error(`OSS GET failed (${response.status}): ${text.slice(0, 200)}`); error.status = response.status; if (response.status === 404 && /\s*NoSuchKey\s*<\/Code>|NoSuchKey/i.test(text)) { error.code = "oss_no_such_key"; } throw error; } return response.text(); } async function putObject(objectKey, body, contentType = "application/octet-stream", extraHeaders = {}) { if (!isOssConfigured()) { throw new Error("OSS is not configured"); } const date = new Date().toUTCString(); const authorization = signOssRequest("PUT", objectKey, date, contentType, extraHeaders); const url = `${getOssEndpoint()}/${encodeURIComponent(objectKey).replace(/%2F/g, "/")}`; const response = await fetch(url, { method: "PUT", headers: { Date: date, Authorization: authorization, "Content-Type": contentType, ...extraHeaders }, body, }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new Error(`OSS PUT failed (${response.status}): ${text.slice(0, 200)}`); } return { ossKey: objectKey, url }; } module.exports = { getObject, putObject, isOssConfigured, createSignedReadUrl };