first commit
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* AWS Signature Version 4 for S3 requests — zero external deps, built on
|
||||
* `node:crypto` + `fetch`. Matches the framework's zero-dep ethos (like
|
||||
* `@wrnexus/ai`) and works with any S3-compatible service (AWS, Cloudflare R2,
|
||||
* Backblaze B2, MinIO, DigitalOcean Spaces).
|
||||
*
|
||||
* Reference: docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
||||
*/
|
||||
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
|
||||
/** Hex-encoded SHA-256 of a payload. */
|
||||
export function sha256Hex(data: Uint8Array | string): string {
|
||||
return createHash("sha256").update(data).digest("hex");
|
||||
}
|
||||
|
||||
function hmac(key: Uint8Array | string, data: string): Buffer {
|
||||
return createHmac("sha256", key as Buffer)
|
||||
.update(data, "utf8")
|
||||
.digest();
|
||||
}
|
||||
|
||||
/** `2024-01-02T03:04:05.678Z` → `20240102T030405Z`. */
|
||||
function amzDate(d: Date): string {
|
||||
return d
|
||||
.toISOString()
|
||||
.replace(/[.]\d{3}/, "")
|
||||
.replace(/[:-]/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent-encode an S3 object key for the request path. Every character except
|
||||
* the RFC 3986 unreserved set is encoded; `/` between segments is preserved.
|
||||
*/
|
||||
export function encodeKey(key: string): string {
|
||||
return key
|
||||
.split("/")
|
||||
.map((seg) =>
|
||||
encodeURIComponent(seg).replace(
|
||||
/[!*'()]/g,
|
||||
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
|
||||
),
|
||||
)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export interface SignInput {
|
||||
method: string;
|
||||
host: string;
|
||||
/** Canonical URI — already `%`-encoded, begins with `/`. */
|
||||
path: string;
|
||||
region: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
/** Hex SHA-256 of the body, or `"UNSIGNED-PAYLOAD"`. */
|
||||
payloadHash: string;
|
||||
/** Extra headers to sign (e.g. `content-type`). `host`/`x-amz-*` are added here. */
|
||||
headers?: Record<string, string>;
|
||||
date: Date;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the signed header set for an S3 request. Returns the headers to send
|
||||
* (lowercased names, including `authorization`, `host`, `x-amz-date`,
|
||||
* `x-amz-content-sha256`).
|
||||
*/
|
||||
export function signS3(input: SignInput): Record<string, string> {
|
||||
const service = input.service ?? "s3";
|
||||
const now = amzDate(input.date);
|
||||
const stamp = now.slice(0, 8);
|
||||
|
||||
// Normalize headers to lowercase names + trimmed values; add the required ones.
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(input.headers ?? {})) {
|
||||
headers[k.toLowerCase()] = String(v).trim();
|
||||
}
|
||||
headers["host"] = input.host;
|
||||
headers["x-amz-content-sha256"] = input.payloadHash;
|
||||
headers["x-amz-date"] = now;
|
||||
|
||||
const names = Object.keys(headers).sort();
|
||||
const canonicalHeaders = names.map((n) => `${n}:${headers[n]}\n`).join("");
|
||||
const signedHeaders = names.join(";");
|
||||
|
||||
const canonicalRequest = [
|
||||
input.method.toUpperCase(),
|
||||
input.path,
|
||||
"", // canonical query string (none for our object operations)
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
input.payloadHash,
|
||||
].join("\n");
|
||||
|
||||
const scope = `${stamp}/${input.region}/${service}/aws4_request`;
|
||||
const stringToSign = ["AWS4-HMAC-SHA256", now, scope, sha256Hex(canonicalRequest)].join("\n");
|
||||
|
||||
const kDate = hmac("AWS4" + input.secretAccessKey, stamp);
|
||||
const kRegion = hmac(kDate, input.region);
|
||||
const kService = hmac(kRegion, service);
|
||||
const kSigning = hmac(kService, "aws4_request");
|
||||
const signature = createHmac("sha256", kSigning).update(stringToSign, "utf8").digest("hex");
|
||||
|
||||
headers["authorization"] =
|
||||
`AWS4-HMAC-SHA256 Credential=${input.accessKeyId}/${scope}, ` +
|
||||
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
||||
|
||||
return headers;
|
||||
}
|
||||
Reference in New Issue
Block a user