127 lines
3.5 KiB
Plaintext
127 lines
3.5 KiB
Plaintext
/* global global, window, module */
|
|
const { sha3_512: sha3 } = require("@noble/hashes/sha3");
|
|
|
|
const defaultLength = 24;
|
|
const bigLength = 32;
|
|
|
|
const createEntropy = (length = 4, random = Math.random) => {
|
|
let entropy = "";
|
|
|
|
while (entropy.length < length) {
|
|
entropy = entropy + Math.floor(random() * 36).toString(36);
|
|
}
|
|
return entropy;
|
|
};
|
|
|
|
/*
|
|
* Adapted from https://github.com/juanelas/bigint-conversion
|
|
* MIT License Copyright (c) 2018 Juan Hernández Serrano
|
|
*/
|
|
function bufToBigInt(buf) {
|
|
let bits = 8n;
|
|
|
|
let value = 0n;
|
|
for (const i of buf.values()) {
|
|
const bi = BigInt(i);
|
|
value = (value << bits) + bi;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
const hash = (input = "") => {
|
|
// Drop the first character because it will bias the histogram
|
|
// to the left.
|
|
return bufToBigInt(sha3(input)).toString(36).slice(1);
|
|
};
|
|
|
|
const alphabet = Array.from({ length: 26 }, (x, i) =>
|
|
String.fromCharCode(i + 97)
|
|
);
|
|
|
|
const randomLetter = (random) =>
|
|
alphabet[Math.floor(random() * alphabet.length)];
|
|
|
|
/*
|
|
This is a fingerprint of the host environment. It is used to help
|
|
prevent collisions when generating ids in a distributed system.
|
|
If no global object is available, you can pass in your own, or fall back
|
|
on a random string.
|
|
*/
|
|
const createFingerprint = ({
|
|
globalObj = typeof global !== "undefined"
|
|
? global
|
|
: typeof window !== "undefined"
|
|
? window
|
|
: {},
|
|
} = {}) => {
|
|
const globals = Object.keys(globalObj).toString();
|
|
const sourceString = globals.length
|
|
? globals + createEntropy(bigLength)
|
|
: createEntropy(bigLength);
|
|
|
|
return hash(sourceString).substring(0, bigLength);
|
|
};
|
|
|
|
const createCounter = (count) => () => {
|
|
return count++;
|
|
};
|
|
|
|
// ~22k hosts before 50% chance of initial counter collision
|
|
// with a remaining counter range of 9.0e+15 in JavaScript.
|
|
const initialCountMax = 476782367;
|
|
|
|
const init = ({
|
|
// Fallback if the user does not pass in a CSPRNG. This should be OK
|
|
// because we don't rely solely on the random number generator for entropy.
|
|
// We also use the host fingerprint, current time, and a session counter.
|
|
random = Math.random,
|
|
counter = createCounter(Math.floor(random() * initialCountMax)),
|
|
length = defaultLength,
|
|
fingerprint = createFingerprint(),
|
|
} = {}) => {
|
|
return function cuid2() {
|
|
const firstLetter = randomLetter(random);
|
|
|
|
// If we're lucky, the `.toString(36)` calls may reduce hashing rounds
|
|
// by shortening the input to the hash function a little.
|
|
const time = Date.now().toString(36);
|
|
const count = counter().toString(36);
|
|
|
|
// The salt should be long enough to be globally unique across the full
|
|
// length of the hash. For simplicity, we use the same length as the
|
|
// intended id output.
|
|
const salt = createEntropy(length, random);
|
|
const hashInput = `${time + salt + count + fingerprint}`;
|
|
|
|
return `${firstLetter + hash(hashInput).substring(1, length)}`;
|
|
};
|
|
};
|
|
|
|
const createId = init();
|
|
|
|
const isCuid = (id, { minLength = 2, maxLength = bigLength } = {}) => {
|
|
const length = id.length;
|
|
const regex = /^[0-9a-z]+$/;
|
|
|
|
try {
|
|
if (
|
|
typeof id === "string" &&
|
|
length >= minLength &&
|
|
length <= maxLength &&
|
|
regex.test(id)
|
|
)
|
|
return true;
|
|
} finally {
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
module.exports.getConstants = () => ({ defaultLength, bigLength });
|
|
module.exports.init = init;
|
|
module.exports.createId = createId;
|
|
module.exports.bufToBigInt = bufToBigInt;
|
|
module.exports.createCounter = createCounter;
|
|
module.exports.createFingerprint = createFingerprint;
|
|
module.exports.isCuid = isCuid;
|