Refuse to boot without R2 credentials
`?? ""` on the access key and secret meant a deployment with no object storage configured started happily, and `POST /jobs` handed clients presigned URLs signed with an empty key. The first sign of trouble was a 403 from R2 - either in the client's uploader or, worse, inside a gateway that had downloaded R2's XML error document in place of a print. Nothing in that chain said "R2 is not configured". Reads both credentials through a `required()` that throws, naming the variable, mirroring auth/verifier.ts and auth/edge_secret.ts. The endpoint is read with `||` rather than `??`, because the committed template ships `R2_ENDPOINT=` empty and `??` only catches null - so an empty value was taken as a real endpoint and the account-id derivation never ran. That is the trap auth/edge_secret.ts documents, and it caught this line too, in the first draft of it. A test covers the derivation. Each boot-failure test fills every other variable so the one under test is the only thing missing; otherwise whichever variable is read first wins and the assertion passes for the wrong reason. e2e-edge.sh gains dummy values, since it boots the backend. It presigns nothing, so they only have to exist.
This commit is contained in:
+51
-6
@@ -1,16 +1,61 @@
|
||||
import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
/**
|
||||
* Object storage for job files, configured once at module load so a
|
||||
* misconfigured deployment fails at boot rather than on the first upload -
|
||||
* the same shape as `auth/verifier.ts` and `auth/edge_secret.ts`, and for
|
||||
* the same reason.
|
||||
*
|
||||
* It replaces `?? ""` on the access key and secret. An empty credential is
|
||||
* not a usable one, so that default bought nothing except moving the failure
|
||||
* somewhere much worse: the process started happily, `POST /jobs` handed a
|
||||
* client a presigned URL signed with an empty key, and the first sign that
|
||||
* anything was wrong was a 403 from R2 - either in the client's uploader or,
|
||||
* worse, inside a gateway that had downloaded R2's XML error document
|
||||
* instead of a print. Nothing in the chain said "R2 is not configured".
|
||||
*
|
||||
* `R2_BUCKET` keeps a default, because a bucket name is not a credential and
|
||||
* "continuum" is a reasonable one. `R2_ENDPOINT` keeps its derivation from
|
||||
* the account id, which is how Cloudflare's own URLs are formed.
|
||||
*/
|
||||
function required(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`${name} is not set - continuum-backend cannot store or serve job files without it. ` +
|
||||
"See .env.example; an empty value is not a usable credential.",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const accountId = process.env.R2_ACCOUNT_ID ?? "";
|
||||
const bucket = process.env.R2_BUCKET ?? "continuum";
|
||||
// `||`, so an empty value in the template falls back rather than becoming a
|
||||
// bucket named "".
|
||||
const bucket = process.env.R2_BUCKET || "continuum";
|
||||
|
||||
// Read before the client is built, so the error names the missing variable
|
||||
// rather than surfacing later as a signing failure.
|
||||
const accessKeyId = required("R2_ACCESS_KEY_ID");
|
||||
const secretAccessKey = required("R2_SECRET_ACCESS_KEY");
|
||||
|
||||
// `||` and not `??`: the committed template ships `R2_ENDPOINT=` empty, and
|
||||
// `??` only catches null and undefined - so an empty value would be taken as
|
||||
// a real endpoint and the derivation below would never run. This is the same
|
||||
// trap `auth/edge_secret.ts` documents, and it caught this line too.
|
||||
const endpoint =
|
||||
process.env.R2_ENDPOINT || (accountId ? `https://${accountId}.r2.cloudflarestorage.com` : "");
|
||||
if (!endpoint) {
|
||||
throw new Error(
|
||||
"Neither R2_ENDPOINT nor R2_ACCOUNT_ID is set - there is no object storage URL to sign against.",
|
||||
);
|
||||
}
|
||||
|
||||
export const s3 = new S3Client({
|
||||
region: "auto",
|
||||
endpoint: process.env.R2_ENDPOINT ?? `https://${accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID ?? "",
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? "",
|
||||
},
|
||||
endpoint,
|
||||
credentials: { accessKeyId, secretAccessKey },
|
||||
});
|
||||
|
||||
/** Presigned PUT for clients uploading a G-code/3MF file directly to R2. */
|
||||
|
||||
Reference in New Issue
Block a user