@11ty/eleventy-utils
Version:
Low level internal utilities to be shared amongst Eleventy projects
158 lines (130 loc) • 3.67 kB
JavaScript
const { base64UrlSafe } = require("./Url.js");
const { isBuffer } = require("./Buffer.js");
const sha256 = require("./lib-sha256.js");
function hasNodeCryptoModule() {
try {
require("node:crypto");
return true;
} catch(e) {
return false;
}
}
const HAS_NODE_CRYPTO = hasNodeCryptoModule();
class Hash {
static create() {
if(typeof globalThis.crypto === "undefined") {
// Backwards compat with Node Crypto, since WebCrypto (crypto global) is Node 20+
if(HAS_NODE_CRYPTO) {
return NodeCryptoHash;
}
return ScriptHash;
}
return WebCryptoHash;
}
// Does not use WebCrypto (as WebCrypto is async-only)
static createSync() {
if(HAS_NODE_CRYPTO) {
return NodeCryptoHash;
}
return ScriptHash;
}
static toBase64(bytes) {
let str = Array.from(bytes, (b) => String.fromCodePoint(b)).join("");
// `btoa` Node 16+
return btoa(str);
}
// Thanks https://evanhahn.com/the-best-way-to-concatenate-uint8arrays/ (Public domain)
static mergeUint8Array(...arrays) {
let totalLength = arrays.reduce(
(total, uint8array) => total + uint8array.byteLength,
0
);
let result = new Uint8Array(totalLength);
let offset = 0;
arrays.forEach((uint8array) => {
result.set(uint8array, offset);
offset += uint8array.byteLength;
});
return result;
}
static bufferToBase64Url(hashBuffer) {
return base64UrlSafe(this.toBase64(new Uint8Array(hashBuffer)));
}
static bufferToHex(hashBuffer) {
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
}
class WebCryptoHash extends Hash {
static async toHash(...content) {
let encoder = new TextEncoder();
let input = this.mergeUint8Array(...content.map(c => {
if(isBuffer(c)) {
return c;
}
return encoder.encode(c);
}));
// `crypto` is Node 20+
return crypto.subtle.digest("SHA-256", input);
}
static async toBase64Url(...content) {
return this.toHash(...content).then(hashBuffer => {
return this.bufferToBase64Url(hashBuffer);
});
}
static async toHex(...content) {
return this.toHash(...content).then(hashBuffer => {
return this.bufferToHex(hashBuffer);
});
}
static toBase64UrlSync() {
throw new Error("Synchronous methods are not available in the Web Crypto API.");
}
static toHexSync() {
throw new Error("Synchronous methods are not available in the Web Crypto API.");
}
}
class NodeCryptoHash extends Hash {
static toHash(...content) {
// This *needs* to be a dynamic require for proper bundling.
const { createHash } = require("node:crypto");
let hash = createHash("sha256");
for(let c of content) {
hash.update(c);
}
return hash;
}
static toBase64Url(...content) {
// Note that Node does include a `digest("base64url")` that is supposedly Node 14+ but curiously failed on Stackblitz’s Node 16.
let base64 = this.toHash(...content).digest("base64");
return base64UrlSafe(base64);
}
static toHex(...content) {
return this.toHash(...content).digest("hex");
}
// aliases
static toBase64UrlSync = this.toBase64Url;
static toHexSync = this.toHex;
}
class ScriptHash extends Hash {
static toHash(...content) {
let hash = sha256();
for(let c of content) {
hash.add(c);
}
return hash.digest();
}
static toBase64Url(...content) {
let hashBuffer = this.toHash(...content);
return this.bufferToBase64Url(hashBuffer);
}
static toHex(...content) {
let hashBuffer = this.toHash(...content);
return this.bufferToHex(hashBuffer);
}
// aliases
static toBase64UrlSync = this.toBase64Url;
static toHexSync = this.toHex;
}
module.exports = { Hash, NodeCryptoHash, ScriptHash, WebCryptoHash }